libmicrohttpd

HTTP/1.x server C library (MHD 1.x, stable)
Log | Files | Refs | Submodules | README | LICENSE

fuzz_postprocessor.c (13175B)


      1 /*
      2   This file is part of libmicrohttpd
      3   Copyright (C) 2026 Christian Grothoff
      4 
      5   This library is free software; you can redistribute it and/or
      6   modify it under the terms of the GNU Lesser General Public
      7   License as published by the Free Software Foundation; either
      8   version 2.1 of the License, or (at your option) any later version.
      9 
     10   This library is distributed in the hope that it will be useful,
     11   but WITHOUT ANY WARRANTY; without even the implied warranty of
     12   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
     13   Lesser General Public License for more details.
     14 
     15   You should have received a copy of the GNU Lesser General Public
     16   License along with this library.
     17   If not, see <http://www.gnu.org/licenses/>.
     18 */
     19 
     20 /**
     21  * @file fuzz/fuzz_postprocessor.c
     22  * @brief Fuzzer for MHD_post_process()
     23  * @author Christian Grothoff
     24  *
     25  * MHD_create_post_processor() only ever looks at the "Content-Type"
     26  * header of the connection it is given, so this harness fabricates the
     27  * minimal connection object instead of pushing a whole request through
     28  * a socket.  That keeps the harness fast and lets the fuzzer control
     29  * three dimensions that matter for the post processor and that a real
     30  * request would not expose directly:
     31  *
     32  *  - the raw Content-Type (encoding and multipart boundary),
     33  *  - the post processor buffer size,
     34  *  - how the POST data is split across MHD_post_process() calls.
     35  *
     36  * Input format:
     37  *   byte 0    content type selector
     38  *   byte 1    post processor buffer size selector
     39  *   byte 2    chunking pattern selector
     40  *   byte 3    length of the boundary taken from the payload
     41  *   byte 4..  the POST data
     42  */
     43 
     44 #define FUZZ_HARNESS_NAME "fuzz_postprocessor"
     45 #include "fuzz_common.h"
     46 
     47 /* internal.h pulls in MHD_config.h and <microhttpd.h> in the right
     48    order; including <microhttpd.h> first would redefine _MHD_EXTERN. */
     49 #include "internal.h"
     50 
     51 static const size_t pp_buf_sizes[] = {
     52   256, 257, 300, 512, 1024, 2048, 4096, 65536
     53 };
     54 
     55 static const size_t chunk_patterns[] = {
     56   1, 2, 3, 5, 7, 13, 32, 64, 1024, 0 /* 0 == everything at once */
     57 };
     58 
     59 static struct MHD_Daemon *shared_daemon;
     60 
     61 /** Statistics, printed at exit with --verbose. */
     62 static unsigned long stat_pp_created;
     63 static unsigned long stat_pp_failed;
     64 static unsigned long stat_values;
     65 
     66 
     67 static enum MHD_Result
     68 dummy_ahc (void *cls,
     69            struct MHD_Connection *connection,
     70            const char *url,
     71            const char *method,
     72            const char *version,
     73            const char *upload_data,
     74            size_t *upload_data_size,
     75            void **req_cls)
     76 {
     77   (void) cls; (void) connection; (void) url; (void) method; (void) version;
     78   (void) upload_data; (void) upload_data_size; (void) req_cls;
     79   return MHD_NO;
     80 }
     81 
     82 
     83 static void
     84 stop_shared_daemon (void)
     85 {
     86   if (NULL != shared_daemon)
     87   {
     88     MHD_stop_daemon (shared_daemon);
     89     shared_daemon = NULL;
     90   }
     91 }
     92 
     93 
     94 static void
     95 print_stats (void)
     96 {
     97   if (! fuzz_verbose)
     98     return;
     99   fprintf (stderr,
    100            "%s: post processors created=%lu rejected=%lu, "
    101            "values reported=%lu\n",
    102            FUZZ_HARNESS_NAME,
    103            stat_pp_created, stat_pp_failed, stat_values);
    104 }
    105 
    106 
    107 static struct MHD_Daemon *
    108 get_shared_daemon (void)
    109 {
    110   if (NULL == shared_daemon)
    111   {
    112     shared_daemon =
    113       MHD_start_daemon (MHD_USE_NO_LISTEN_SOCKET
    114                         | (fuzz_verbose ? MHD_USE_ERROR_LOG : 0u),
    115                         0, NULL, NULL, &dummy_ahc, NULL,
    116                         MHD_OPTION_END);
    117     if (NULL != shared_daemon)
    118     {
    119       (void) atexit (&stop_shared_daemon);
    120       (void) atexit (&print_stats);
    121     }
    122   }
    123   return shared_daemon;
    124 }
    125 
    126 
    127 struct pp_ctx
    128 {
    129   const char *data;
    130   size_t data_len;
    131   uint64_t max_off;
    132 };
    133 
    134 
    135 static enum MHD_Result
    136 post_iter (void *cls,
    137            enum MHD_ValueKind kind,
    138            const char *key,
    139            const char *filename,
    140            const char *content_type,
    141            const char *transfer_encoding,
    142            const char *data,
    143            uint64_t off,
    144            size_t size)
    145 {
    146   struct pp_ctx *ctx = (struct pp_ctx *) cls;
    147   volatile size_t sink = 0;
    148 
    149   (void) kind;
    150   stat_values++;
    151   /* Touch every string the post processor claims to have produced;
    152      ASAN turns any dangling or out-of-bounds pointer into an error. */
    153   if (NULL != key)
    154     sink += strlen (key);
    155   if (NULL != filename)
    156     sink += strlen (filename);
    157   if (NULL != content_type)
    158     sink += strlen (content_type);
    159   if (NULL != transfer_encoding)
    160     sink += strlen (transfer_encoding);
    161   if ( (NULL != data) && (0 != size) )
    162   {
    163     size_t i;
    164 
    165     for (i = 0; i < size; i++)
    166       sink += (size_t) (unsigned char) data[i];
    167   }
    168   else if (NULL == data)
    169   {
    170     if (0 != size)
    171       fuzz_report_finding ("MHD_PostDataIterator called with NULL data but "
    172                            "non-zero size");
    173   }
    174   /* A single value is delivered in order and can never be longer than
    175      the POST data that was fed in. */
    176   if (off + size > ctx->data_len + 1)
    177     fuzz_report_finding ("MHD_PostDataIterator reported more value data "
    178                          "than the POST data contained");
    179   if (off > ctx->max_off + ctx->data_len)
    180     fuzz_report_finding ("MHD_PostDataIterator offset jumped implausibly");
    181   ctx->max_off = off;
    182   (void) sink;
    183   return MHD_YES;
    184 }
    185 
    186 
    187 int
    188 LLVMFuzzerTestOneInput (const uint8_t *data,
    189                         size_t size)
    190 {
    191   struct MHD_Connection c;
    192   struct MHD_HTTP_Req_Header h;
    193   struct MHD_PostProcessor *pp;
    194   struct pp_ctx ctx;
    195   char *ctype;
    196   char *body;
    197   size_t body_len;
    198   size_t buf_size;
    199   size_t chunk;
    200   size_t off;
    201   size_t blen;
    202   unsigned int sel;
    203 
    204   if (size < 5)
    205     return 0;
    206   if (NULL == get_shared_daemon ())
    207     return 0;
    208 
    209   sel = data[0];
    210   buf_size = pp_buf_sizes[data[1] % (sizeof (pp_buf_sizes)
    211                                      / sizeof (pp_buf_sizes[0]))];
    212   chunk = chunk_patterns[data[2] % (sizeof (chunk_patterns)
    213                                     / sizeof (chunk_patterns[0]))];
    214   blen = data[3];
    215 
    216   body_len = size - 4;
    217   if (body_len > 8192)
    218     body_len = 8192;
    219   if (blen > body_len)
    220     blen = body_len;
    221 
    222   /* Exactly sized copies so that ASAN catches reads past the end. */
    223   body = (char *) malloc (body_len + 1);
    224   if (NULL == body)
    225     return 0;
    226   memcpy (body, data + 4, body_len);
    227   body[body_len] = '\0';
    228 
    229   ctype = (char *) malloc (blen + 64);
    230   if (NULL == ctype)
    231   {
    232     free (body);
    233     return 0;
    234   }
    235   switch (sel % 6)
    236   {
    237   case 0:
    238     memcpy (ctype, MHD_HTTP_POST_ENCODING_FORM_URLENCODED,
    239             sizeof (MHD_HTTP_POST_ENCODING_FORM_URLENCODED));
    240     break;
    241   case 1:
    242   case 2:
    243     {
    244       /* multipart with a boundary taken from the payload */
    245       size_t o = 0;
    246       size_t i;
    247 
    248       memcpy (ctype, MHD_HTTP_POST_ENCODING_MULTIPART_FORMDATA,
    249               sizeof (MHD_HTTP_POST_ENCODING_MULTIPART_FORMDATA) - 1);
    250       o = sizeof (MHD_HTTP_POST_ENCODING_MULTIPART_FORMDATA) - 1;
    251       memcpy (ctype + o, "; boundary=", 11);
    252       o += 11;
    253       for (i = 0; i < blen; i++)
    254       {
    255         char ch = body[i];
    256 
    257         /* A NUL would truncate the header value; map it away. */
    258         ctype[o++] = ('\0' == ch) ? 'x' : ch;
    259       }
    260       ctype[o] = '\0';
    261       break;
    262     }
    263   case 3:
    264     (void) snprintf (ctype, blen + 64,
    265                      "multipart/form-data; boundary=\"%.*s\"",
    266                      (int) ((blen > 40) ? 40 : blen), body);
    267     break;
    268   case 4:
    269     memcpy (ctype, MHD_HTTP_POST_ENCODING_MULTIPART_FORMDATA,
    270             sizeof (MHD_HTTP_POST_ENCODING_MULTIPART_FORMDATA));
    271     break;
    272   default:
    273     memcpy (ctype, "text/plain", sizeof ("text/plain"));
    274     break;
    275   }
    276 
    277   memset (&c, 0, sizeof (c));
    278   memset (&h, 0, sizeof (h));
    279   h.header = MHD_HTTP_HEADER_CONTENT_TYPE;
    280   h.header_size = MHD_STATICSTR_LEN_ (MHD_HTTP_HEADER_CONTENT_TYPE);
    281   h.value = ctype;
    282   h.value_size = strlen (ctype);
    283   h.kind = MHD_HEADER_KIND;
    284   c.daemon = shared_daemon;
    285   c.rq.headers_received = &h;
    286   c.rq.headers_received_tail = &h;
    287   c.state = MHD_CONNECTION_HEADERS_PROCESSED;
    288 
    289   ctx.data = body;
    290   ctx.data_len = body_len;
    291   ctx.max_off = 0;
    292 
    293   pp = MHD_create_post_processor (&c, buf_size, &post_iter, &ctx);
    294   if (NULL == pp)
    295   {
    296     stat_pp_failed++;
    297     free (ctype);
    298     free (body);
    299     return 0;
    300   }
    301   stat_pp_created++;
    302 
    303   off = 0;
    304   while (off < body_len)
    305   {
    306     size_t n = (0 == chunk) ? (body_len - off) : chunk;
    307 
    308     if (n > body_len - off)
    309       n = body_len - off;
    310     (void) MHD_post_process (pp, body + off, n);
    311     off += n;
    312   }
    313   /* Final, zero-length call: what MHD itself does at the end of the
    314      upload. */
    315   (void) MHD_post_process (pp, body + body_len, 0);
    316   (void) MHD_destroy_post_processor (pp);
    317 
    318   free (ctype);
    319   free (body);
    320   return 0;
    321 }
    322 
    323 
    324 /* ------------------------------------------------------------------ */
    325 /* Generator                                                           */
    326 /* ------------------------------------------------------------------ */
    327 
    328 static const char *const gen_disp[] = {
    329   "Content-Disposition: form-data; name=\"a\"",
    330   "Content-Disposition: form-data; name=\"a\"; filename=\"f.txt\"",
    331   "Content-Disposition: form-data; name=a",
    332   "Content-Disposition: form-data",
    333   "Content-Disposition: attachment; name=\"a\"",
    334   "Content-Disposition: form-data; name=\"\"",
    335   "Content-Disposition: form-data; name=\"a",
    336   "Content-Type: text/plain",
    337   "Content-Transfer-Encoding: binary",
    338   "X-Other: value"
    339 };
    340 
    341 static const char *const gen_kv[] = {
    342   "a=1", "b=%41", "c", "d=", "=e", "&", "&&", "a=%", "a=%4", "a=%zz",
    343   "verylongkeyname=verylongvaluewithlotsofcharacters", "a+b=c+d",
    344   "%41%42=%43%44"
    345 };
    346 
    347 
    348 static size_t
    349 fuzz_generate (struct fuzz_rng *rng,
    350                uint8_t *buf,
    351                size_t cap)
    352 {
    353   size_t len = 0;
    354   int multipart;
    355   const char *boundary;
    356   unsigned int nparts;
    357   unsigned int i;
    358   static const char *const boundaries[] = {
    359     "--abc", "XY", "boundary", "-", "aa", "0123456789012345678901234567890",
    360     "a\"b"
    361   };
    362 
    363 #define ADD(s) \
    364   do { \
    365     const char *s_ = (s); \
    366     size_t l_ = strlen (s_); \
    367     if (len + l_ >= cap) \
    368     return len; \
    369     memcpy (buf + len, s_, l_); \
    370     len += l_; \
    371   } while (0)
    372 
    373   if (cap < 64)
    374     return 0;
    375   multipart = ! fuzz_chance (rng, 3);
    376   boundary = boundaries[fuzz_below (rng,
    377                                     (uint32_t) (sizeof (boundaries)
    378                                                 / sizeof (char *)))];
    379   buf[len++] = (uint8_t) (multipart ? (1 + fuzz_below (rng, 2)) : 0);
    380   buf[len++] = fuzz_byte (rng);
    381   buf[len++] = fuzz_byte (rng);
    382   buf[len++] = (uint8_t) strlen (boundary);
    383   if (multipart)
    384   {
    385     /* The first strlen(boundary) bytes of the body double as the
    386        boundary in the Content-Type header (see the input format). */
    387     ADD (boundary);
    388     nparts = 1 + fuzz_below (rng, 4);
    389     for (i = 0; i < nparts; i++)
    390     {
    391       unsigned int nhdr = fuzz_below (rng, 3);
    392       unsigned int k;
    393 
    394       ADD ("\r\n--");
    395       ADD (boundary);
    396       ADD ("\r\n");
    397       for (k = 0; k <= nhdr; k++)
    398       {
    399         ADD (gen_disp[fuzz_below (rng,
    400                                   (uint32_t) (sizeof (gen_disp)
    401                                               / sizeof (char *)))]);
    402         ADD ("\r\n");
    403       }
    404       ADD ("\r\n");
    405       {
    406         unsigned int n = fuzz_below (rng, 40);
    407 
    408         for (k = 0; (k < n) && (len < cap); k++)
    409           buf[len++] = (uint8_t) ('A' + fuzz_below (rng, 26));
    410       }
    411     }
    412     ADD ("\r\n--");
    413     ADD (boundary);
    414     ADD (fuzz_chance (rng, 4) ? "\r\n" : "--\r\n");
    415   }
    416   else
    417   {
    418     unsigned int n = 1 + fuzz_below (rng, 10);
    419 
    420     for (i = 0; i < n; i++)
    421     {
    422       if (0 != i)
    423         ADD ("&");
    424       ADD (gen_kv[fuzz_below (rng,
    425                               (uint32_t) (sizeof (gen_kv) / sizeof (char *)))]);
    426     }
    427   }
    428 #undef ADD
    429   return len;
    430 }
    431 
    432 
    433 /* ------------------------------------------------------------------ */
    434 /* Seed corpus                                                         */
    435 /* ------------------------------------------------------------------ */
    436 
    437 struct pp_seed
    438 {
    439   const char *txt;
    440   size_t len;
    441 };
    442 
    443 #define PSEED(t) { t, sizeof (t) - 1 }
    444 
    445 static const struct pp_seed pp_seeds[] = {
    446   PSEED ("\x00\x00\x00\x00" "a=1&b=%41&c"),
    447   PSEED ("\x00\x00\x04\x00" "a=1&b=%41&c"),
    448   PSEED ("\x00\x00\x00\x00" "a=%"),
    449   PSEED ("\x00\x00\x00\x00" "&&&&"),
    450   PSEED ("\x01\x00\x00\x05" "--abc\r\n----abc\r\n"
    451          "Content-Disposition: form-data; name=\"k\"\r\n\r\nvalue\r\n"
    452          "----abc--\r\n"),
    453   PSEED ("\x01\x00\x01\x05" "--abc\r\n----abc\r\n"
    454          "Content-Disposition: form-data; name=\"k\"; filename=\"f\"\r\n"
    455          "Content-Type: text/plain\r\n\r\nvalue\r\n----abc--\r\n"),
    456   PSEED ("\x01\x07\x02\x02" "XY\r\n--XY\r\n\r\nnoheaders\r\n--XY--\r\n"),
    457   PSEED ("\x01\x00\x00\x01" "-\r\n---\r\n\r\nx\r\n-----\r\n"),
    458   PSEED ("\x04\x00\x00\x00" "no boundary at all"),
    459   PSEED ("\x05\x00\x00\x00" "text/plain body"),
    460   PSEED ("\x03\x00\x00\x03" "a\"b\r\n--a\"b\r\n\r\nv\r\n--a\"b--\r\n")
    461 };
    462 
    463 
    464 static size_t
    465 fuzz_seed_count (void)
    466 {
    467   return sizeof (pp_seeds) / sizeof (pp_seeds[0]);
    468 }
    469 
    470 
    471 static const uint8_t *
    472 fuzz_seed_get (size_t idx,
    473                size_t *len)
    474 {
    475   *len = pp_seeds[idx].len;
    476   return (const uint8_t *) pp_seeds[idx].txt;
    477 }