libmicrohttpd2

HTTP server C library (MHD 2.x, alpha)
Log | Files | Refs | README | LICENSE

test_oom.c (17405B)


      1 /* SPDX-License-Identifier: LGPL-2.1-or-later OR (GPL-2.0-or-later WITH eCos-exception-2.0) */
      2 /*
      3   This file is part of GNU libmicrohttpd.
      4   Copyright (C) 2025 Christian Grothoff
      5 
      6   GNU libmicrohttpd is free software; you can redistribute it and/or
      7   modify it under the terms of the GNU Lesser General Public
      8   License as published by the Free Software Foundation; either
      9   version 2.1 of the License, or (at your option) any later version.
     10 
     11   GNU libmicrohttpd is distributed in the hope that it will be useful,
     12   but WITHOUT ANY WARRANTY; without even the implied warranty of
     13   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
     14   Lesser General Public License for more details.
     15 
     16   Alternatively, you can redistribute GNU libmicrohttpd and/or
     17   modify it under the terms of the GNU General Public License as
     18   published by the Free Software Foundation; either version 2 of
     19   the License, or (at your option) any later version, together
     20   with the eCos exception, as follows:
     21 
     22     As a special exception, if other files instantiate templates or
     23     use macros or inline functions from this file, or you compile this
     24     file and link it with other works to produce a work based on this
     25     file, this file does not by itself cause the resulting work to be
     26     covered by the GNU General Public License. However the source code
     27     for this file must still be made available in accordance with
     28     section (3) of the GNU General Public License v2.
     29 
     30     This exception does not invalidate any other reasons why a work
     31     based on this file might be covered by the GNU General Public
     32     License.
     33 
     34   You should have received copies of the GNU Lesser General Public
     35   License and the GNU General Public License along with this library;
     36   if not, see <https://www.gnu.org/licenses/>.
     37 */
     38 
     39 /**
     40  * @file test_oom.c
     41  * @brief tests handling of memory pool exhaustion
     42  * @author Christian Grothoff
     43  */
     44 #include <stdio.h>
     45 #include <stdbool.h>
     46 #include <errno.h>
     47 #include <string.h>
     48 #include <stdlib.h>
     49 #include <unistd.h>
     50 #include <arpa/inet.h>
     51 #include <netinet/ip.h>
     52 #include "microhttpd2.h"
     53 
     54 
     55 /**
     56  * How big do we make the MHD buffer? Use a small value so we
     57  * can trigger OOM in a reasonable amount of time.
     58  */
     59 #define BUFFER_SIZE 2048
     60 
     61 /**
     62  * What is the step size. Should eventually use 1, but
     63  * as long as we get tons of failures, a larger step size
     64  * is probably nicer.
     65  */
     66 #define STEP 71
     67 
     68 /**
     69  * Our port.
     70  */
     71 static uint16_t port;
     72 
     73 /**
     74  * Set to true once we hit the out-of-memory condition.
     75  */
     76 static bool out_of_memory;
     77 
     78 /**
     79  * Callback used by libmicrohttpd in order to obtain content.  The
     80  * callback is to copy at most @a max bytes of content into @a buf or
     81  * provide zero-copy data for #MHD_DCC_action_continue_zc().
     82  *
     83  * @param dyn_cont_cls closure argument to the callback
     84  * @param ctx the context to produce the action to return,
     85  *            the pointer is only valid until the callback returns
     86  * @param pos position in the datastream to access;
     87  *        note that if a `struct MHD_Response` object is re-used,
     88  *        it is possible for the same content reader to
     89  *        be queried multiple times for the same data;
     90  *        however, if a `struct MHD_Response` is not re-used,
     91  *        libmicrohttpd guarantees that "pos" will be
     92  *        the sum of all data sizes provided by this callback
     93  * @param[out] buf where to copy the data
     94  * @param max maximum number of bytes to copy to @a buf (size of @a buf),
     95               if the size of the content of the response is known then size
     96               of the buffer is never larger than amount of the content left
     97  * @return action to use,
     98  *         NULL in case of any error (the response will be aborted)
     99  */
    100 static const struct MHD_DynamicContentCreatorAction *
    101 dyn_cc (void *dyn_cont_cls,
    102         struct MHD_DynamicContentCreatorContext *ctx,
    103         uint_fast64_t pos,
    104         void *buf,
    105         size_t max)
    106 {
    107   int *flag = dyn_cont_cls;
    108   struct MHD_NameValueCStr footer = {
    109     .name = "Footer",
    110     .value = "Value"
    111   };
    112 
    113   if (0 == *flag)
    114     return MHD_DCC_action_finish_with_footer (ctx,
    115                                               0,
    116                                               &footer);
    117   (*flag) = 0;
    118   memset (buf,
    119           'a',
    120           max);
    121   return MHD_DCC_action_continue (ctx,
    122                                   max);
    123 }
    124 
    125 
    126 /**
    127  * This method is called by libmicrohttpd when response with dynamic content
    128  * is being destroyed.  It should be used to free resources associated
    129  * with the dynamic content.
    130  *
    131  * @param[in] free_cls closure
    132  * @ingroup response
    133  */
    134 static void
    135 dyn_cc_free (void *free_cls)
    136 {
    137   free (free_cls);
    138 }
    139 
    140 
    141 /**
    142  * Function to process data uploaded by a client.
    143  *
    144  * @param upload_cls the argument given together with the function
    145  *                   pointer when the handler was registered with MHD
    146  * @param request the request is being processed
    147  * @param content_data_size the size of the @a content_data,
    148  *                          zero when all data have been processed
    149  * @param[in] content_data the uploaded content data,
    150  *                         may be modified in the callback,
    151  *                         valid only until return from the callback,
    152  *                         NULL when all data have been processed
    153  * @return action specifying how to proceed:
    154  *         #MHD_upload_action_continue() to continue upload (for incremental
    155  *         upload processing only),
    156  *         #MHD_upload_action_suspend() to stop reading the upload until
    157  *         the request is resumed,
    158  *         #MHD_upload_action_abort_request() to close the socket,
    159  *         or a response to discard the rest of the upload and transmit
    160  *         the response
    161  * @ingroup action
    162  */
    163 static const struct MHD_UploadAction *
    164 upload_cb (void *upload_cls,
    165            struct MHD_Request *request,
    166            size_t content_data_size,
    167            void *content_data)
    168 {
    169   int *flag;
    170 
    171   (void)upload_cls;
    172   (void)content_data_size;
    173   (void)content_data;
    174   flag = malloc (sizeof (int));
    175   if (NULL == flag)
    176   {
    177     fprintf (stderr,
    178              "Failed to allocate memory.\n");
    179     exit (99);
    180   }
    181   *flag = 1;
    182 
    183   return MHD_upload_action_from_response (
    184     request,
    185     MHD_response_from_callback (MHD_HTTP_STATUS_OK,
    186                                 MHD_SIZE_UNKNOWN,
    187                                 &dyn_cc,
    188                                 flag,
    189                                 &dyn_cc_free));
    190 }
    191 
    192 
    193 /**
    194  * A client has requested the given url using the given method
    195  * (#MHD_HTTP_METHOD_GET, #MHD_HTTP_METHOD_PUT,
    196  * #MHD_HTTP_METHOD_DELETE, #MHD_HTTP_METHOD_POST, etc).
    197  * If @a upload_size is not zero and response action is provided by this
    198  * callback, then upload will be discarded and the stream (the connection for
    199  * HTTP/1.1) will be closed after sending the response.
    200  *
    201  * @param cls argument given together with the function
    202  *        pointer when the handler was registered with MHD
    203  * @param request the request object
    204  * @param path the requested uri (without arguments after "?")
    205  * @param method the HTTP method used (#MHD_HTTP_METHOD_GET,
    206  *        #MHD_HTTP_METHOD_PUT, etc.)
    207  * @param upload_size the size of the message upload content payload,
    208  *                    #MHD_SIZE_UNKNOWN for chunked uploads (if the
    209  *                    final chunk has not been processed yet)
    210  * @return action how to proceed, NULL
    211  *         if the request must be aborted due to a serious
    212  *         error while handling the request (implies closure
    213  *         of underling data stream, for HTTP/1.1 it means
    214  *         socket closure).
    215  */
    216 static const struct MHD_Action *
    217 server_req_cb (void *cls,
    218                struct MHD_Request *MHD_RESTRICT request,
    219                const struct MHD_String *MHD_RESTRICT path,
    220                enum MHD_HTTP_Method method,
    221                uint_fast64_t upload_size)
    222 {
    223   (void)cls;
    224   (void)path;
    225   (void)method;
    226   (void)upload_size;
    227   return MHD_action_process_upload_full (request,
    228                                          upload_size,
    229                                          &upload_cb,
    230                                          NULL);
    231 }
    232 
    233 
    234 /**
    235  * Helper function to deal with partial writes.
    236  * Fails hard (calls exit() on failures)!
    237  *
    238  * @param fd where to write to
    239  * @param buf what to write
    240  * @param buf_size number of bytes in @a buf
    241  */
    242 static void
    243 write_all (int fd,
    244            const void *buf,
    245            size_t buf_size)
    246 {
    247   const char *cbuf = (const char *)buf;
    248   size_t off;
    249 
    250   off = 0;
    251   while (off < buf_size)
    252   {
    253     ssize_t ret;
    254 
    255     ret = write (fd,
    256                  &cbuf[off],
    257                  buf_size - off);
    258     if (ret <= 0)
    259     {
    260       fprintf (stderr,
    261                "Writing %u bytes to %d failed: %s\n",
    262                (unsigned int)(buf_size - off),
    263                fd,
    264                strerror (errno));
    265       exit (1);
    266     }
    267     off += (size_t)ret;
    268   }
    269 }
    270 
    271 
    272 static int
    273 run_test (unsigned int url_len,
    274           unsigned int query_len,
    275           unsigned int header_len,
    276           unsigned int cookie_len,
    277           unsigned int body_len)
    278 {
    279   char filler[BUFFER_SIZE + 1];
    280   int s;
    281 
    282   out_of_memory = false;
    283   memset (filler,
    284           'a',
    285           BUFFER_SIZE);
    286   filler[BUFFER_SIZE] = '\0'; /* just to be conservative */
    287   s = socket (AF_INET,
    288               SOCK_STREAM,
    289               0);
    290   if (-1 == s)
    291   {
    292     fprintf (stderr,
    293              "socket() failed: %s\n",
    294              strerror (errno));
    295     return -1;
    296   }
    297 
    298   {
    299     struct sockaddr_in sa = {
    300       .sin_family = AF_INET,
    301       .sin_port = htons (port),
    302     };
    303     inet_pton (AF_INET,
    304                "127.0.0.1",
    305                &sa.sin_addr);
    306     if (0 != connect (s,
    307                       (struct sockaddr *)&sa,
    308                       sizeof (sa)))
    309     {
    310       fprintf (stderr,
    311                "bind() failed: %s\n",
    312                strerror (errno));
    313       close (s);
    314       return -1;
    315     }
    316   }
    317 
    318   {
    319     char upload[BUFFER_SIZE * 2];
    320     int iret;
    321 
    322     iret = snprintf (upload,
    323                      sizeof (upload),
    324                      "PUT /%.*s?q=%.*s HTTP/1.0\r\n"
    325                      "Content-Length: %u\r\n"
    326                      "Key: %.*s\r\n"
    327                      "Cookie: a=%.*s\r\n\r\n"
    328                      "%.*s",
    329                      (int)url_len,
    330                      filler,
    331                      (int)query_len,
    332                      filler,
    333                      body_len,
    334                      (int)header_len,
    335                      filler,
    336                      (int)cookie_len,
    337                      filler,
    338                      (int)body_len,
    339                      filler);
    340     if ((-1 == iret)
    341         || (((size_t)iret) > sizeof (upload)))
    342     {
    343       fprintf (stderr,
    344                "failed to build request buffer: %d\n",
    345                iret);
    346       close (s);
    347       return -1;
    348     }
    349     write_all (s,
    350                upload,
    351                strlen (upload));
    352   }
    353   /* read and discard response */
    354   {
    355     bool got_data = false;
    356     bool nice = false;
    357     char dummy[16 * 1024];
    358     int flags = 0;
    359 
    360     while (1)
    361     {
    362       ssize_t res;
    363 
    364       res = recv (s,
    365                   &dummy,
    366                   sizeof (dummy),
    367                   flags);
    368       flags = MSG_DONTWAIT;
    369       if (res > 0)
    370       {
    371         got_data = true;
    372         dummy[res] = '\0';
    373         /* FIXME: allow other "too large" responses to also count as
    374            'nice' here */
    375         if (NULL !=
    376             strstr (dummy,
    377                     "431 Request Header Fields Too Large"))
    378           nice = true;
    379       }
    380       if (res <= 0)
    381         break;
    382     }
    383     if (nice)
    384       out_of_memory = true;
    385     if (!got_data)
    386     {
    387       out_of_memory = true;
    388       fprintf (stderr,
    389                "Response was not nice (%u/%u/%u/%u/%u)\n",
    390                url_len,
    391                query_len,
    392                header_len,
    393                cookie_len,
    394                body_len);
    395     }
    396   }
    397   close (s);
    398   return out_of_memory ? 1 : 0;
    399 }
    400 
    401 
    402 static int
    403 test_url (void)
    404 {
    405   bool oom_hit;
    406 
    407   oom_hit = false;
    408   for (unsigned int i = 0;
    409        i < BUFFER_SIZE;
    410        i += STEP)
    411   {
    412     int ret;
    413 
    414     ret = run_test (i, 0, 0, 0, 0);
    415     if (-1 == ret)
    416     {
    417       return 1;
    418     }
    419     if (1 == ret)
    420     {
    421       oom_hit = true;
    422     }
    423     if ((oom_hit) && (1 != ret))
    424     {
    425       fprintf (stderr,
    426                "Strange: OOM stopped at %u after being hit earlier (url)?\n",
    427                i);
    428     }
    429   }
    430   if (!oom_hit)
    431   {
    432     fprintf (stderr,
    433              "Failed to trigger OOM condition via URL\n");
    434     return 1;
    435   }
    436   return 0;
    437 }
    438 
    439 
    440 static int
    441 test_query (void)
    442 {
    443   bool oom_hit;
    444 
    445   oom_hit = false;
    446   for (unsigned int i = 0;
    447        i < BUFFER_SIZE;
    448        i += STEP)
    449   {
    450     int ret;
    451 
    452     ret = run_test (0, i, 0, 0, 0);
    453     if (-1 == ret)
    454     {
    455       return 1;
    456     }
    457     if (1 == ret)
    458     {
    459       oom_hit = true;
    460     }
    461     if ((oom_hit) && (1 != ret))
    462     {
    463       fprintf (stderr,
    464                "Strange: OOM stopped at %u after being hit earlier (query)?\n",
    465                i);
    466     }
    467   }
    468   if (!oom_hit)
    469   {
    470     fprintf (stderr,
    471              "Failed to trigger OOM condition via query\n");
    472     return 1;
    473   }
    474   return 0;
    475 }
    476 
    477 
    478 static int
    479 test_header (void)
    480 {
    481   bool oom_hit;
    482 
    483   oom_hit = false;
    484   for (unsigned int i = 0;
    485        i < BUFFER_SIZE;
    486        i += STEP)
    487   {
    488     int ret;
    489 
    490     ret = run_test (0, 0, i, 0, 0);
    491     if (-1 == ret)
    492     {
    493       return 1;
    494     }
    495     if (1 == ret)
    496     {
    497       oom_hit = true;
    498     }
    499     if ((oom_hit) && (1 != ret))
    500     {
    501       fprintf (stderr,
    502                "Strange: OOM stopped at %u after being hit earlier (header)?\n",
    503                i);
    504     }
    505   }
    506   if (!oom_hit)
    507   {
    508     fprintf (stderr,
    509              "Failed to trigger OOM condition via header\n");
    510     return 1;
    511   }
    512   return 0;
    513 }
    514 
    515 
    516 static int
    517 test_cookie (void)
    518 {
    519   bool oom_hit;
    520 
    521   oom_hit = false;
    522   for (unsigned int i = 0;
    523        i < BUFFER_SIZE;
    524        i += STEP)
    525   {
    526     int ret;
    527 
    528     ret = run_test (0, 0, 0, i, 0);
    529     if (-1 == ret)
    530     {
    531       return 1;
    532     }
    533     if (1 == ret)
    534     {
    535       oom_hit = true;
    536     }
    537     if ((oom_hit) && (1 != ret))
    538     {
    539       fprintf (stderr,
    540                "Strange: OOM stopped at %u after being hit earlier (cookie)?\n",
    541                i);
    542     }
    543   }
    544   if (!oom_hit)
    545   {
    546     fprintf (stderr,
    547              "Failed to trigger OOM condition via cookie\n");
    548     return 1;
    549   }
    550   return 0;
    551 }
    552 
    553 
    554 static int
    555 test_body (void)
    556 {
    557   bool oom_hit;
    558 
    559   oom_hit = false;
    560   for (unsigned int i = 0;
    561        i < BUFFER_SIZE;
    562        i += STEP)
    563   {
    564     int ret;
    565 
    566     ret = run_test (0, 0, 0, 0, i);
    567     if (-1 == ret)
    568     {
    569       return 1;
    570     }
    571     if (1 == ret)
    572     {
    573       oom_hit = true;
    574     }
    575     if ((oom_hit) && (1 != ret))
    576     {
    577       fprintf (stderr,
    578                "Strange: OOM stopped at %u after being hit earlier (body)?\n",
    579                i);
    580     }
    581   }
    582   if (!oom_hit)
    583   {
    584     fprintf (stderr,
    585              "Failed to trigger OOM condition via body\n");
    586     return 1;
    587   }
    588   return 0;
    589 }
    590 
    591 
    592 static int
    593 test_mix (void)
    594 {
    595   bool oom_hit;
    596 
    597   /* mix and match path */
    598   for (unsigned int i = 0;
    599        i < BUFFER_SIZE;
    600        i += STEP)
    601   {
    602     int ret;
    603 
    604     ret = run_test (i / 5 + 1, i / 5 + 1, i / 5 + 1, i / 5 + 1, i / 5 + 1);
    605     if (-1 == ret)
    606     {
    607       return 1;
    608     }
    609     if (1 == ret)
    610     {
    611       oom_hit = true;
    612     }
    613     if ((oom_hit) && (1 != ret))
    614     {
    615       fprintf (stderr,
    616                "Strange: OOM stopped at %u after being hit earlier (mix)?\n",
    617                i);
    618     }
    619   }
    620   if (!oom_hit)
    621   {
    622     fprintf (stderr,
    623              "Failed to trigger OOM condition in mix-and-match\n");
    624     return 1;
    625   }
    626 
    627 
    628   return 0;
    629 }
    630 
    631 
    632 static int
    633 run_tests (void)
    634 {
    635   int ret = 0;
    636 
    637 #if 1
    638   ret |= test_url ();
    639   ret |= test_query ();
    640   ret |= test_header ();
    641   ret |= test_cookie ();
    642   ret |= test_body ();
    643   ret |= test_mix ();
    644 #endif
    645   return ret;
    646 }
    647 
    648 
    649 static void
    650 no_log (void *cls,
    651         enum MHD_StatusCode sc,
    652         const char *fm,
    653         va_list ap)
    654 {
    655   (void)cls;
    656   (void)sc;
    657   (void)fm;
    658   (void)ap;
    659 
    660   /* intentionally empty */
    661 }
    662 
    663 
    664 int
    665 main (void)
    666 {
    667   struct MHD_Daemon *d;
    668 
    669   d = MHD_daemon_create (&server_req_cb,
    670                          NULL);
    671   if (MHD_SC_OK !=
    672       MHD_DAEMON_SET_OPTIONS (
    673         d,
    674         MHD_D_OPTION_WM_WORKER_THREADS (2),
    675         MHD_D_OPTION_LOG_CALLBACK (&no_log, NULL),
    676         MHD_D_OPTION_CONN_MEMORY_LIMIT (BUFFER_SIZE),
    677         MHD_D_OPTION_DEFAULT_TIMEOUT_MILSEC (1500),
    678         MHD_D_OPTION_BIND_PORT (MHD_AF_AUTO,
    679                                 0)))
    680   {
    681     fprintf (stderr,
    682              "Failed to configure daemon!");
    683     return 1;
    684   }
    685 
    686   {
    687     enum MHD_StatusCode sc;
    688 
    689     sc = MHD_daemon_start (d);
    690     if (MHD_SC_OK != sc)
    691     {
    692 #ifdef FIXME_STATUS_CODE_TO_STRING_NOT_IMPLEMENTED
    693       fprintf (stderr,
    694                "Failed to start server: %s\n",
    695                MHD_status_code_to_string_lazy (sc));
    696 #else
    697       fprintf (stderr,
    698                "Failed to start server: %u\n",
    699                (unsigned int)sc);
    700 #endif
    701       MHD_daemon_destroy (d);
    702       return 1;
    703     }
    704   }
    705 
    706   {
    707     union MHD_DaemonInfoFixedData info;
    708     enum MHD_StatusCode sc;
    709 
    710     sc = MHD_daemon_get_info_fixed (
    711       d,
    712       MHD_DAEMON_INFO_FIXED_BIND_PORT,
    713       &info);
    714     if (MHD_SC_OK != sc)
    715     {
    716       fprintf (stderr,
    717                "Failed to determine our port: %u\n",
    718                (unsigned int)sc);
    719       MHD_daemon_destroy (d);
    720       return 1;
    721     }
    722     port = info.v_bind_port_uint16;
    723   }
    724 
    725   {
    726     int result;
    727 
    728     result = run_tests ();
    729     MHD_daemon_destroy (d);
    730     return result;
    731   }
    732 }