libmicrohttpd

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

fuzz_str.c (15904B)


      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_str.c
     22  * @brief Direct in-process fuzzer for the string primitives of mhd_str.c
     23  * @author Christian Grothoff
     24  *
     25  * Every destination buffer is obtained with malloc() at *exactly* the
     26  * size that the documented contract of the function under test
     27  * requires -- never a generous stack array.  Under AddressSanitizer the
     28  * redzone immediately after the allocation turns any one-byte
     29  * over-write into a hard error, which is precisely the property a
     30  * fixed-size stack buffer would not give us.
     31  *
     32  * In addition to the contract-exact calls, several functions are also
     33  * called with a deliberately *too small* output buffer: functions that
     34  * take a buffer size argument must detect this and must not write past
     35  * the end.
     36  *
     37  * Input format:
     38  *   byte 0    target selector
     39  *   byte 1    auxiliary parameter (buffer size shrink, split point, ...)
     40  *   byte 2..  payload
     41  */
     42 
     43 #define FUZZ_HARNESS_NAME "fuzz_str"
     44 #include "fuzz_common.h"
     45 
     46 #include "mhd_options.h"
     47 #include "mhd_str.h"
     48 
     49 /**
     50  * Largest digest MHD supports; this is the size of the on-stack buffers
     51  * that digestauth.c passes to MHD_hex_to_bin().
     52  */
     53 #define MODEL_MAX_DIGEST 32
     54 
     55 /**
     56  * Model the (pre-2026) digestauth.c call site that fed a client
     57  * controlled, up to 4 * digest_size characters long 'response' value
     58  * into MHD_hex_to_bin() together with a digest_size byte buffer.
     59  *
     60  * MHD_hex_to_bin() has no output size parameter: it writes len/2 bytes,
     61  * so a caller with a fixed size buffer *must* bound the input length.
     62  * This target replays that call site with an exactly sized heap buffer
     63  * so that ASAN reports the overflow.  It is OFF by default (it models a
     64  * caller, not the library) -- enable it with MHD_FUZZ_MODEL_DIGEST_SINK=1
     65  * to reproduce the primitive behind the digest 'response' overflow.
     66  */
     67 static int model_digest_sink;
     68 static int model_digest_sink_read;
     69 
     70 enum str_target
     71 {
     72   TGT_HEX_TO_BIN = 0,
     73   TGT_BIN_TO_HEX,
     74   TGT_BIN_TO_HEX_Z,
     75   TGT_PCT_STRICT,
     76   TGT_PCT_LENIENT,
     77   TGT_PCT_IN_PLACE_STRICT,
     78   TGT_PCT_IN_PLACE_LENIENT,
     79   TGT_UNQUOTE,
     80   TGT_QUOTE,
     81   TGT_BASE64,
     82   TGT_TO_UINT64,
     83   TGT_TOKENS,
     84   TGT_EQUAL_CASELESS,
     85   TGT_DIGEST_HEX_SINK,
     86   TGT_COUNT
     87 };
     88 
     89 
     90 /**
     91  * Allocate exactly @a n bytes.  Zero-sized allocations are turned into
     92  * a one byte allocation so that the returned pointer stays valid, but
     93  * the harness never tells the library about that extra byte.
     94  */
     95 static void *
     96 xalloc (size_t n)
     97 {
     98   void *p = malloc ((0 == n) ? 1 : n);
     99 
    100   if (NULL == p)
    101     abort ();
    102   return p;
    103 }
    104 
    105 
    106 static char *
    107 dup_z (const uint8_t *d,
    108        size_t n)
    109 {
    110   char *p = (char *) xalloc (n + 1);
    111 
    112   memcpy (p, d, n);
    113   p[n] = '\0';
    114   return p;
    115 }
    116 
    117 
    118 int
    119 LLVMFuzzerTestOneInput (const uint8_t *data,
    120                         size_t size)
    121 {
    122   enum str_target tgt;
    123   const uint8_t *p;
    124   size_t n;
    125   unsigned int aux;
    126 
    127   if (size < 3)
    128     return 0;
    129   tgt = (enum str_target) (data[0] % (unsigned int) TGT_COUNT);
    130   aux = data[1];
    131   p = data + 2;
    132   n = size - 2;
    133   /* Keep the inputs small: these are unit-level primitives and short
    134      inputs explore the interesting corner cases far more efficiently. */
    135   if (n > 512)
    136     n = 512;
    137 
    138   switch (tgt)
    139   {
    140   case TGT_HEX_TO_BIN:
    141     {
    142       /* Contract: the output buffer must be len/2 bytes long, or
    143          len/2 + 1 if len is odd. */
    144       size_t need = (n / 2) + (n % 2);
    145       uint8_t *out = (uint8_t *) xalloc (need);
    146       size_t r = MHD_hex_to_bin ((const char *) p, n, out);
    147 
    148       if (r > need)
    149         fuzz_report_finding ("MHD_hex_to_bin() reported more bytes written "
    150                              "than its documented output size");
    151       free (out);
    152       break;
    153     }
    154 
    155   case TGT_BIN_TO_HEX:
    156     {
    157       char *out = (char *) xalloc (2 * n);
    158       size_t r = MHD_bin_to_hex (p, n, out);
    159 
    160       if (r != 2 * n)
    161         fuzz_report_finding ("MHD_bin_to_hex() did not write 2 * size chars");
    162       free (out);
    163       break;
    164     }
    165 
    166   case TGT_BIN_TO_HEX_Z:
    167     {
    168       char *out = (char *) xalloc (2 * n + 1);
    169       size_t r = MHD_bin_to_hex_z (p, n, out);
    170 
    171       if (r != 2 * n)
    172         fuzz_report_finding ("MHD_bin_to_hex_z() did not write 2 * size chars");
    173       if ('\0' != out[r])
    174         fuzz_report_finding ("MHD_bin_to_hex_z() result is not "
    175                              "zero-terminated");
    176       free (out);
    177       break;
    178     }
    179 
    180   case TGT_PCT_STRICT:
    181     {
    182       /* First with the exact maximum size, then with a buffer that is
    183          deliberately too small. */
    184       char *out = (char *) xalloc (n);
    185       size_t r = MHD_str_pct_decode_strict_n_ ((const char *) p, n, out, n);
    186       size_t small;
    187 
    188       if (r > n)
    189         fuzz_report_finding ("MHD_str_pct_decode_strict_n_() overran the "
    190                              "documented output size");
    191       free (out);
    192       small = (0 == n) ? 0 : (n * (aux % 100u)) / 100u;
    193       out = (char *) xalloc (small);
    194       (void) MHD_str_pct_decode_strict_n_ ((const char *) p, n, out, small);
    195       free (out);
    196       break;
    197     }
    198 
    199   case TGT_PCT_LENIENT:
    200     {
    201       char *out = (char *) xalloc (n);
    202       bool broken = false;
    203       size_t r =
    204         MHD_str_pct_decode_lenient_n_ ((const char *) p, n, out, n, &broken);
    205       size_t small;
    206 
    207       if (r > n)
    208         fuzz_report_finding ("MHD_str_pct_decode_lenient_n_() overran the "
    209                              "documented output size");
    210       free (out);
    211       small = (0 == n) ? 0 : (n * (aux % 100u)) / 100u;
    212       out = (char *) xalloc (small);
    213       (void) MHD_str_pct_decode_lenient_n_ ((const char *) p, n, out, small,
    214                                             NULL);
    215       free (out);
    216       break;
    217     }
    218 
    219   case TGT_PCT_IN_PLACE_STRICT:
    220     {
    221       char *s = dup_z (p, n);
    222       size_t r = MHD_str_pct_decode_in_place_strict_ (s);
    223 
    224       if (r > n)
    225         fuzz_report_finding ("MHD_str_pct_decode_in_place_strict_() grew "
    226                              "the string");
    227       free (s);
    228       break;
    229     }
    230 
    231   case TGT_PCT_IN_PLACE_LENIENT:
    232     {
    233       char *s = dup_z (p, n);
    234       bool broken = false;
    235       size_t r = MHD_str_pct_decode_in_place_lenient_ (s, &broken);
    236 
    237       if (r > n)
    238         fuzz_report_finding ("MHD_str_pct_decode_in_place_lenient_() grew "
    239                              "the string");
    240       free (s);
    241       break;
    242     }
    243 
    244 #ifdef DAUTH_SUPPORT
    245   case TGT_UNQUOTE:
    246     {
    247       /* Unquoting never grows the string. */
    248       char *out = (char *) xalloc (n);
    249       size_t r = MHD_str_unquote ((const char *) p, n, out);
    250 
    251       if (r > n)
    252         fuzz_report_finding ("MHD_str_unquote() wrote more characters than "
    253                              "the quoted input had");
    254       free (out);
    255       break;
    256     }
    257 #else  /* ! DAUTH_SUPPORT */
    258   case TGT_UNQUOTE:
    259     break;
    260 #endif /* ! DAUTH_SUPPORT */
    261 
    262 #if defined(DAUTH_SUPPORT) || defined(BAUTH_SUPPORT)
    263   case TGT_QUOTE:
    264     {
    265       /* Quoting can at most double the size. */
    266       char *out = (char *) xalloc (2 * n);
    267       size_t r = MHD_str_quote ((const char *) p, n, out, 2 * n);
    268       size_t small;
    269 
    270       if (r > 2 * n)
    271         fuzz_report_finding ("MHD_str_quote() wrote more than 2 * len chars");
    272       free (out);
    273       small = (0 == n) ? 0 : (n * (aux % 200u)) / 100u;
    274       out = (char *) xalloc (small);
    275       (void) MHD_str_quote ((const char *) p, n, out, small);
    276       free (out);
    277       break;
    278     }
    279 #else  /* ! (DAUTH_SUPPORT || BAUTH_SUPPORT) */
    280   case TGT_QUOTE:
    281     break;
    282 #endif /* ! (DAUTH_SUPPORT || BAUTH_SUPPORT) */
    283 
    284 #ifdef BAUTH_SUPPORT
    285   case TGT_BASE64:
    286     {
    287       size_t need = MHD_base64_max_dec_size_ (n);
    288       uint8_t *out = (uint8_t *) xalloc (need);
    289       size_t r = MHD_base64_to_bin_n ((const char *) p, n, out, need);
    290       size_t small;
    291 
    292       if (r > need)
    293         fuzz_report_finding ("MHD_base64_to_bin_n() exceeded "
    294                              "MHD_base64_max_dec_size_()");
    295       free (out);
    296       small = (0 == need) ? 0 : (need * (aux % 100u)) / 100u;
    297       out = (uint8_t *) xalloc (small);
    298       (void) MHD_base64_to_bin_n ((const char *) p, n, out, small);
    299       free (out);
    300       break;
    301     }
    302 #else  /* ! BAUTH_SUPPORT */
    303   case TGT_BASE64:
    304     break;
    305 #endif /* ! BAUTH_SUPPORT */
    306 
    307   case TGT_TO_UINT64:
    308     {
    309       char *s = dup_z (p, n);
    310       uint64_t v1 = 0;
    311       uint64_t v2 = 0;
    312       size_t r1 = MHD_str_to_uint64_n_ (s, n, &v1);
    313       size_t r2 = MHD_strx_to_uint64_n_ (s, n, &v2);
    314 
    315       if (r1 > n)
    316         fuzz_report_finding ("MHD_str_to_uint64_n_() consumed more than "
    317                              "maxlen characters");
    318       if (r2 > n)
    319         fuzz_report_finding ("MHD_strx_to_uint64_n_() consumed more than "
    320                              "maxlen characters");
    321       free (s);
    322       break;
    323     }
    324 
    325   case TGT_TOKENS:
    326     {
    327       /* The "token" arguments have documented preconditions (no NUL,
    328          space, tab or comma); a fuzzer that violates them would only
    329          find its own mistakes, so sanitise them here. */
    330       char *s = dup_z (p, n);
    331       size_t split = (0 == n) ? 0 : (aux % n);
    332       char *tok = dup_z (p + split, n - split);
    333       size_t tlen;
    334       ssize_t bs;
    335       char *out;
    336       size_t k;
    337 
    338       for (k = 0; k < n - split; k++)
    339       {
    340         if ( ('\0' == tok[k]) || (' ' == tok[k]) ||
    341              ('\t' == tok[k]) || (',' == tok[k]) )
    342           tok[k] = 'x';
    343       }
    344       tlen = strlen (tok);
    345       if (0 == tlen)
    346       {
    347         free (tok);
    348         tok = dup_z ((const uint8_t *) "chunked", 7);
    349         tlen = 7;
    350       }
    351       (void) MHD_str_has_token_caseless_ (s, tok, tlen);
    352 
    353       /* Documented worst case growth of the output is 50%. */
    354       bs = (ssize_t) (n + n / 2 + 1);
    355       out = (char *) xalloc ((size_t) bs);
    356       (void) MHD_str_remove_token_caseless_ (s, n, tok, tlen, out, &bs);
    357       if (bs > (ssize_t) (n + n / 2 + 1))
    358         fuzz_report_finding ("MHD_str_remove_token_caseless_() reported a "
    359                              "result larger than the documented 50% growth");
    360       if (0 <= bs)
    361       {
    362         /* The output of the previous call is normalised, which is the
    363            documented precondition of the in-place variant. */
    364         char *norm = dup_z ((const uint8_t *) out, (size_t) bs);
    365         size_t nlen = (size_t) bs;
    366 
    367         (void) MHD_str_remove_tokens_caseless_ (norm, &nlen, tok, tlen);
    368         if (nlen > (size_t) bs)
    369           fuzz_report_finding ("MHD_str_remove_tokens_caseless_() grew the "
    370                                "string");
    371         free (norm);
    372       }
    373       free (out);
    374       free (tok);
    375       free (s);
    376       break;
    377     }
    378 
    379   case TGT_EQUAL_CASELESS:
    380     {
    381       size_t split = (0 == n) ? 0 : (aux % n);
    382       char *a = dup_z (p, split);
    383       char *b = dup_z (p + split, n - split);
    384 
    385       (void) MHD_str_equal_caseless_bin_n_ (a, b, (split < n - split)
    386                                             ? split : (n - split));
    387       (void) MHD_str_equal_caseless_n_ (a, b, n);
    388       free (a);
    389       free (b);
    390       break;
    391     }
    392 
    393   case TGT_DIGEST_HEX_SINK:
    394     {
    395       /* See the comment on model_digest_sink above. */
    396       size_t len = n;
    397       uint8_t *out;
    398 
    399       if (! model_digest_sink_read)
    400       {
    401         const char *e = getenv ("MHD_FUZZ_MODEL_DIGEST_SINK");
    402 
    403         model_digest_sink_read = 1;
    404         if (NULL != e)
    405           model_digest_sink = (0 != atoi (e));
    406       }
    407       if (! model_digest_sink)
    408         break;
    409       /* digestauth.c only checked 'len <= 4 * digest_size' before the
    410          fix; replay exactly that bound. */
    411       if (len > 4 * MODEL_MAX_DIGEST)
    412         len = 4 * MODEL_MAX_DIGEST;
    413       out = (uint8_t *) xalloc (MODEL_MAX_DIGEST);
    414       (void) MHD_hex_to_bin ((const char *) p, len, out);
    415       free (out);
    416       break;
    417     }
    418 
    419   case TGT_COUNT:
    420   default:
    421     break;
    422   }
    423   return 0;
    424 }
    425 
    426 
    427 /* ------------------------------------------------------------------ */
    428 /* Generator                                                           */
    429 /* ------------------------------------------------------------------ */
    430 
    431 static const char *const gen_str_atoms[] = {
    432   "%41", "%", "%%", "%zz", "%0", "%00", "%ff", "\\", "\\\"", "\"",
    433   "0123456789abcdef", "0123456789ABCDEF", "ffffffffffffffffffffffff",
    434   "gg", "0x", "18446744073709551615", "99999999999999999999",
    435   "a, b, c", "chunked", "identity", " , ", ",,", "token",
    436   "QUJD", "QQ==", "Q===", "====", "AAAA", "AA=A",
    437   "\x00\x01\x7f\x80\xff", "  ", "\t", "\r\n"
    438 };
    439 
    440 
    441 static size_t
    442 fuzz_generate (struct fuzz_rng *rng,
    443                uint8_t *buf,
    444                size_t cap)
    445 {
    446   size_t len = 0;
    447   unsigned int natoms;
    448   unsigned int i;
    449 
    450   if (cap < 8)
    451     return 0;
    452   buf[len++] = (uint8_t) fuzz_below (rng, (uint32_t) TGT_COUNT);
    453   buf[len++] = fuzz_byte (rng);
    454   natoms = 1 + fuzz_below (rng, 12);
    455   for (i = 0; i < natoms; i++)
    456   {
    457     if (fuzz_chance (rng, 3))
    458     {
    459       /* a run of hex digits of a length that is interesting for the
    460          digest code paths (32, 64, 128 characters) */
    461       unsigned int k;
    462       unsigned int run = 1 + fuzz_below (rng, 140);
    463 
    464       for (k = 0; (k < run) && (len < cap); k++)
    465         buf[len++] = (uint8_t) "0123456789abcdef"[fuzz_below (rng, 16)];
    466     }
    467     else if (fuzz_chance (rng, 8))
    468     {
    469       if (len < cap)
    470         buf[len++] = fuzz_byte (rng);
    471     }
    472     else
    473     {
    474       const char *a =
    475         gen_str_atoms[fuzz_below (rng,
    476                                   (uint32_t) (sizeof (gen_str_atoms)
    477                                               / sizeof (char *)))];
    478       size_t al = strlen (a);
    479 
    480       if (len + al > cap)
    481         break;
    482       memcpy (buf + len, a, al);
    483       len += al;
    484     }
    485   }
    486   return len;
    487 }
    488 
    489 
    490 /* ------------------------------------------------------------------ */
    491 /* Seed corpus                                                         */
    492 /* ------------------------------------------------------------------ */
    493 
    494 struct str_seed
    495 {
    496   const char *txt;
    497   size_t len;
    498 };
    499 
    500 #define SSEED(t) { t, sizeof (t) - 1 }
    501 
    502 static const struct str_seed str_seeds[] = {
    503   /* hex -> bin, 128 characters: the length digestauth.c used to allow
    504      into a 32 byte buffer */
    505   SSEED ("\x00\x00"
    506          "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
    507          "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"),
    508   SSEED ("\x00\x00" "0123456789abcdef0123456789abcdef"),
    509   SSEED ("\x00\x00" "abc"),
    510   SSEED ("\x00\x00" "zz"),
    511   SSEED ("\x01\x00" "\x01\x02\x03\x04"),
    512   SSEED ("\x02\x00" "\xff\xfe"),
    513   SSEED ("\x03\x00" "/a%41%42%zz%"),
    514   SSEED ("\x04\x40" "/a%41%42%zz%"),
    515   SSEED ("\x05\x00" "%41%42%%%0"),
    516   SSEED ("\x06\x00" "%41%42%%%0"),
    517   SSEED ("\x07\x00" "a\\\"b\\\\c"),
    518   SSEED ("\x08\x00" "a\"b\\c"),
    519   SSEED ("\x09\x00" "QUJDRA=="),
    520   SSEED ("\x09\x00" "QUJDR==="),
    521   SSEED ("\x0a\x00" "18446744073709551615"),
    522   SSEED ("\x0a\x00" "ffffffffffffffff"),
    523   SSEED ("\x0b\x03" "chunked, identity, chunked"),
    524   SSEED ("\x0c\x04" "CHUNKEDchunked"),
    525   SSEED ("\x0d\x00"
    526          "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
    527          "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef")
    528 };
    529 
    530 
    531 static size_t
    532 fuzz_seed_count (void)
    533 {
    534   return sizeof (str_seeds) / sizeof (str_seeds[0]);
    535 }
    536 
    537 
    538 static const uint8_t *
    539 fuzz_seed_get (size_t idx,
    540                size_t *len)
    541 {
    542   *len = str_seeds[idx].len;
    543   return (const uint8_t *) str_seeds[idx].txt;
    544 }