libmicrohttpd

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

README (17794B)


      1 Seed corpus for the src/fuzz harnesses
      2 ======================================
      3 
      4 `<harness>-NN.bin` is the built-in seed corpus of that harness, dumped
      5 to disk.  Regenerate at any time with
      6 
      7     ./fuzz_<harness> --write-corpus=<this directory>
      8 
      9 or, from the build tree,
     10 
     11     make -C src/fuzz refresh-corpus
     12 
     13 Replay everything (this is what a CI regression run should do):
     14 
     15     make -C src/fuzz check-corpus
     16     # or, per harness:
     17     ./fuzz_request --corpus-dir=src/fuzz/corpus
     18 
     19 Files belonging to another harness are simply uninteresting inputs for
     20 the harness that reads them, so pointing every harness at the whole
     21 directory is fine and gives some extra cross-pollination.
     22 
     23 The set is kept minimal: every seed here adds coverage that no other
     24 seed provides.  After changing a seed table, re-check that with
     25 
     26     <target> -merge=1 <empty dir> <this directory>
     27 
     28 from an OSS-Fuzz style build (contrib/oss-fuzz/build.sh); a seed the
     29 merge does not copy across is redundant and should be deleted from the
     30 table rather than left in the corpus.  Note that fuzz_request is
     31 mildly non-deterministic -- MHD timestamps its nonces and its
     32 connection timeouts -- so a single merge decision worth ~0.1% of the
     33 regions is noise; only drop a seed that several independent merges
     34 agree on.
     35 
     36 
     37 fuzz_request seeds
     38 ------------------
     39 
     40   00  content-length-body              body oracle, Content-Length
     41   01  chunked-with-extensions          body oracle, chunk extensions
     42                                        -> chunk-extension CRLF bug
     43   02  chunked-split                    body oracle, chunk boundary split
     44                                        across two send() calls
     45   03  small-pool-trailing-query-arg    128..512 byte connection pool, no
     46                                        header lines, "?novalue"
     47                                        -> read-buffer shift-back bug
     48   04  small-pool-trailing-query-arg-2  same with "?a=1&b"
     49   05  digest-unknown-algorithm         algorithm=BOGUS
     50                                        -> MHD_DIGEST_AUTH_ALGO3_INVALID
     51                                           MHD_PANIC()
     52   06  digest-overlong-response         401 challenge, then a replay on a
     53                                        fresh connection with the
     54                                        harvested nonce and a 128 hex
     55                                        digit response=
     56                                        -> stack buffer overflow
     57   07  digest-userhash                  userhash=true with a 128 char
     58                                        username
     59   08  basic-auth                       Authorization: Basic
     60   09  multipart-post                   chunked multipart/form-data
     61   10  urlencoded-post                  application/x-www-form-urlencoded
     62   11  folded-header                    obs-fold continuation line
     63   12  pipelined                        two requests in one segment
     64 
     65 Seeds 00-12 leave the API-selection block (bytes 4-9, see section 2.2.1
     66 of ../README) zero and only drive the request parser.  Seeds 13-39 each
     67 switch on one area of it; without them those entry points are reachable
     68 only by guessing six configuration bytes.
     69 
     70   13  ext-callback-chunked             MHD_SIZE_UNKNOWN content reader,
     71                                        response headers and a trailer
     72   14  ext-callback-error               known-length reader that fails
     73                                        part way through
     74   15  ext-fd-response                  MHD_create_response_from_fd()
     75   16  ext-fd-at-offset-response        ..._from_fd_at_offset()
     76   17  ext-pipe-response                ..._from_pipe()
     77   18  ext-iovec-response               ..._from_iovec()
     78   19  ext-empty-response-hdrapi        ..._empty() plus get/del header
     79                                        and MHD_set_response_options()
     80   20  ext-digest-check-digest3         pre-computed userdigest path
     81   21  ext-digest-v1-wrappers           MHD_digest_auth_check()
     82   22  ext-digest-request-info          MHD_digest_auth_get_request_info3()
     83   23  ext-external-event-loop          MHD_get_fdset2()/run_from_select2()
     84                                        plus the timeout accessors
     85   24  ext-external-event-loop-v1       the v1 fdset/run_from_select pair
     86   25  ext-connection-api               every introspection accessor
     87   26  ext-suspend-resume               suspend, resume from the pump loop
     88   27  ext-suspend-two-connections      two connections parked at once
     89   28  ext-upgrade                      101 + MHD_upgrade_action()
     90   29  ext-buffer-persistent            buffer/MHD_RESPMEM_PERSISTENT
     91   30  ext-buffer-free-callback         ..._with_free_callback()
     92   31  ext-from-data                    MHD_create_response_from_data()
     93   32  ext-fd64-response                ..._from_fd64()
     94   33  ext-basic-auth-v1                MHD_basic_auth_get_username_password()
     95   34  ext-basic-auth-challenge         MHD_queue_basic_auth_fail_response()
     96   35  ext-basic-auth-challenge-utf8    ..._response3() with UTF-8 charset
     97   36  ext-digest-check-digest-v1       MHD_digest_auth_check_digest()
     98   37  ext-digest-check-digest2         ..._digest2()  (never with
     99                                        MHD_DIGEST_ALG_AUTO, see K8)
    100   38  ext-digest-get-username-v1       MHD_digest_auth_get_username()
    101   39  ext-digest-get-username3         ..._username3()
    102 
    103 fuzz_str seeds
    104 --------------
    105 
    106   Cover MHD_hex_to_bin (including the 128 character input that
    107   digestauth.c used to allow into a 32 byte buffer), MHD_bin_to_hex[_z],
    108   the percent-decoders (strict, lenient, in place), MHD_str_unquote,
    109   MHD_str_quote, MHD_base64_to_bin_n, MHD_str[x]_to_uint64_n_ and the
    110   token helpers.
    111 
    112 fuzz_auth_header seeds
    113 ----------------------
    114 
    115   Well-formed and broken Digest parameter lists (unknown algorithm,
    116   extended `username*` notation, unterminated quoted strings, empty
    117   parameter list, over-long values) plus Basic `token68` variants, and
    118   -- with bit 0x02 of byte 0 -- the connection-less digest helpers at
    119   several algorithms and several deliberately undersized output
    120   buffers.
    121 
    122 fuzz_postprocessor seeds
    123 ------------------------
    124 
    125   urlencoded bodies with broken percent escapes, multipart bodies with
    126   ordinary, degenerate ("-") and quote-containing boundaries, a
    127   multipart Content-Type without a boundary, and a non-POST
    128   Content-Type.
    129 
    130 
    131 distilled/
    132 ----------
    133 
    134 The edge-minimal residue of the fuzzing campaigns: 2562 inputs, 240 KB,
    135 named `<harness>-dNNN.bin` and routed by that prefix exactly like the
    136 seeds above.  Files are numbered in the order the cover picked them, so
    137 `-d000` is the highest-gain input of its harness.
    138 
    139 Unlike `<harness>-NN.bin` these are *not* generated from a built-in
    140 table, so `refresh-corpus` neither writes nor clobbers them; and unlike
    141 `known-findings/` they are not hand-edited and carry no individual
    142 meaning.  Do not document them one by one -- the set is only ever
    143 regenerated wholesale.
    144 
    145 Provenance: an 8 hour, 12 core, 1.62 billion execution campaign
    146 (ASan+UBSan, libFuzzer, hourly restarts) starting from the seed corpus
    147 above.  The campaign corpus was 18 226 inputs / 12 MB, which is too much
    148 to carry in a source tree, so it was reduced by greedy set cover over
    149 *edges* rather than by `-merge=1`, which minimises for features and
    150 keeps roughly seven times as many files:
    151 
    152     <target> -merge=1 -merge_control_file=mcf.txt <empty dir> <corpus>
    153 
    154 writes a `COV` line per input, and the cover is computed from those
    155 offline.  Because these harnesses are mildly non-deterministic, the
    156 cover is built only from edges that two independent merges agree an
    157 input reproduces -- the same "several independent merges" rule stated at
    158 the top of this file.
    159 
    160 The `fuzz_eventloop` and `fuzz_tls` inputs come from a second, 1 hour /
    161 4 core / 17.0 million execution campaign (same sanitizers, same engine,
    162 `--enable-https` so that `fuzz_tls` could be built at all -- note that
    163 `contrib/oss-fuzz/build.sh` configures `--disable-https`, which is why
    164 `fuzz_tls` had no distilled inputs before and started that campaign from
    165 its seed table alone).  It ran after the PSK scenario and the
    166 queue-and-stop scenario were added to those two harnesses, and its
    167 starting corpus was this directory plus the first campaign's residue, so
    168 what it contributed is beyond what 8 core-hours per harness had already
    169 reached: `fuzz_tls` 6355 -> 6844 edges, `fuzz_eventloop` 7406 -> 7655.
    170 The other six harnesses were not re-run; their inputs are unchanged.
    171 
    172 A `COV` line holds only the edges its input was *first* to reach in that
    173 merge, so what a cover has to pay for depends on what it is credited
    174 with already having.  Credit it with everything else that stays in the
    175 tree -- both seed sets and the distilled inputs of the harnesses not
    176 being regenerated -- because `check-corpus` replays all three
    177 directories through every harness, and an input is worth carrying only
    178 if it adds an edge none of them reach.  Doing that costs nothing and is
    179 worth roughly a third of the files: the `fuzz_tls` cover needed 490
    180 edges and 167 inputs against the whole tree, against 1580 edges and 282
    181 inputs when credited with the seed corpus alone -- identical coverage
    182 either way, because the other harnesses' inputs were already reaching
    183 those 1090 edges.  Compute every harness's cover against the same base
    184 so that no two of them depend on each other.
    185 
    186 **An engine-produced corpus is not pristine, and this directory has to
    187 be filtered before it can be committed.**  `fuzz_request` inputs carry a
    188 ground-truth body declaration in their `op == 1` segment, and the body
    189 oracle is armed only for a `fuzz_pristine` input precisely because
    190 "random mutations invalidate such declarations" (`fuzz_common.h`).
    191 libFuzzer mutates that segment independently of the wire segments, so a
    192 campaign corpus is full of inputs whose declaration no longer describes
    193 their own request -- 383 of 6884 here, 5.6%.  Under libFuzzer that is
    194 harmless: `-DFUZZ_NO_MAIN` compiles out every assignment to
    195 `fuzz_pristine`, so the oracle never arms and the campaign never
    196 notices.  But `--corpus-dir` sets `fuzz_pristine = 1` for *every* file it
    197 reads, so replaying such an input reports a "request body desync"
    198 against a declaration that was never true -- a false positive, and one
    199 that looks exactly like a request-smuggling finding.
    200 
    201 Committing the raw cover therefore broke `make check-corpus` with 42
    202 such reports.  The fix is to sweep every candidate with
    203 `--file=` first and exclude the ones that fire *before* computing the
    204 cover, which is what produced the set in this directory; all 2562 files
    205 replay clean through all eight harnesses.  Anyone replaying a downloaded
    206 ClusterFuzz corpus locally will hit the same thing, and it is not a bug
    207 in MHD.
    208 
    209 Sweep every candidate anyway, but expect this particular exclusion only
    210 for `fuzz_request`: it is the sole reader of `fuzz_pristine`, so it is
    211 the only harness that can report a desync against its own stale
    212 declaration.  The second campaign's 15 554 candidates produced no
    213 rejects at all for that reason.
    214 
    215 The set reaches ~99% of the first campaign's edges, not 100%: 3763/3798
    216 for fuzz_request, and exactly 100% for the four direct-API harnesses,
    217 which are deterministic.  Two thirds of that shortfall is the exclusion
    218 above -- 64 edges were reached only by inputs with a stale declaration
    219 -- and the rest is edges no single input reproduces reliably.  Forcing
    220 the latter in by adding every input that covers a rare edge was tried
    221 and moved the number by one, so that part is inherent to minimising
    222 rather than a fixable omission.  Trading 1% of edges for 86% fewer files
    223 is the intended bargain.  Do not chase it.
    224 
    225 The second campaign lost nothing: `fuzz_eventloop` ends at 7655 of the
    226 7656 edges its campaign corpus reaches, and `fuzz_tls` at 6844 against
    227 that corpus' 6770 -- above it, because a `fuzz_tls` cover measured
    228 inside the tree also gets the other harnesses' inputs, which are
    229 uninteresting to it but not inert.  Both harnesses are deterministic
    230 enough here that all three `-runs=0` measurements agreed to within two
    231 edges.
    232 
    233 To regenerate after a long campaign: keep the campaign corpus, sweep it
    234 for oracle-firing inputs, run two independent merges over what is left,
    235 recompute the cover against everything that stays in the tree, and
    236 replace the affected harnesses' files wholesale.  Check the result the
    237 same way it was checked here -- replay both the campaign corpus and the
    238 distilled set with `-runs=0` three times each and compare the
    239 `INITED cov:` figures, because a single measurement of either is worth a
    240 few edges of noise -- and finish with `make check-corpus`.
    241 
    242 Measure *every* harness before and after, not just the ones being
    243 regenerated: dropping an input removes it from the other seven as well.
    244 Replacing the 320 first-campaign `fuzz_eventloop` inputs with 248 new
    245 ones moved the other six harnesses by at most three edges, which is
    246 noise, but that is a thing to confirm rather than assume.  Note also
    247 that `make check-corpus` is a weaker gate than it looks on a tree
    248 configured without `--enable-asserts`, where `mhd_assert()` compiles
    249 away entirely; the campaign build has them, so inputs that survived the
    250 merges have already been through the stronger one.
    251 
    252 
    253 known-findings/
    254 ---------------
    255 
    256 Reproducers for the issues that this suite found in MHD itself; see
    257 section 6 of ../README, which records the status of each.  All of them
    258 are fixed on master, so all of them replay clean, and `check-corpus`
    259 replays this directory too.
    260 
    261 A file is named `K<n>-<harness>-<what>.bin` for the harness that found
    262 it; the ones without a harness in the name predate that convention and
    263 are all fuzz_request inputs.  `contrib/oss-fuzz/make_seed_corpus.sh`
    264 routes each into the seed corpus of its own harness on that basis.
    265 
    266 They are kept as a separate, explicitly named set rather than being
    267 folded into the main corpus because the files there are *generated*: a
    268 hand-written input would be clobbered or renumbered by the next
    269 `refresh-corpus`.  Replay one with
    270 
    271     ./fuzz_request --file=src/fuzz/corpus/known-findings/K1-digest-empty-realm.bin
    272 
    273   K1-digest-empty-realm.bin          digestauth.c is_param_equal()
    274                                      mhd_assert (0 != param->value.len)
    275   K2-chunkext-no-space.bin           connection.c handle_recv_no_space()
    276   K3-chunkext-stop-with-error.bin    connection.c
    277                                      transmit_error_response_len()
    278   K4a-wsp-first-header.bin           connection.c get_req_header(),
    279                                      CLIENT_DISCIPLINE_LVL <= -1
    280   K4b-empty-header-name.bin          connection.c get_req_header(),
    281                                      CLIENT_DISCIPLINE_LVL <= -2
    282   K5-bare-cr-keep.bin                connection.c get_req_header(),
    283                                      CLIENT_DISCIPLINE_LVL == -3
    284   K6-nonce-length-collision.bin      digestauth.c check_nonce_nc()
    285                                      mhd_assert (0 == nn->nonce[noncelen])
    286   K7-upgrade-after-must-close.bin    connection.c build_header_response()
    287                                      mhd_assert (upgrade -> MUST_UPGRADE);
    288                                      a single request carrying both
    289                                      Content-Length and chunked
    290                                      Transfer-Encoding, then an upgrade
    291   K9-fuzz_postprocessor-             postprocessor.c post_process_multipart()
    292     nested-boundary-leak.bin         leaked the previous nested boundary
    293                                      on every extra nested
    294                                      multipart/mixed part
    295   K14-fuzz_eventloop-                daemon.c call_handlers() asserted a
    296     force-close-not-closed.bin       post-condition that
    297                                      MHD_connection_handle_read() does
    298                                      not guarantee when the read buffer
    299                                      is full
    300                                      *** still open, see ../../../patches ***
    301                                      Only fires on an --enable-asserts
    302                                      *ASan* build: the redzones change
    303                                      the pool arithmetic that reaches it.
    304   K15-fuzz_eventloop-              daemon.c close_all_connections()
    305     stop-with-queued-connection.bin  asserted that only a daemon with an
    306                                      internal polling thread can have
    307                                      connections queued by
    308                                      MHD_add_connection(), while the code
    309                                      that fills that list queues for any
    310                                      thread-safe daemon.  An external
    311                                      event loop that adds a connection and
    312                                      stops before the next MHD_run()
    313                                      aborts on an --enable-asserts build.
    314 
    315 These files are *not* regenerated by `--write-corpus`; they are edited by
    316 hand.  When the input format changes they have to be migrated, and the
    317 migration has to be checked -- replay each one with `--verbose` before
    318 and after and confirm the daemon, handler, body and challenge counts are
    319 unchanged.  The last such change made the API-selection block
    320 unconditional, which is six zero bytes inserted at offset 4 for K1-K6.
    321 
    322 K8 has no reproducer here: its trigger is an argument the application
    323 chooses, not network input, so the harness never performs it.  See
    324 section 6 of ../README for how to reach it by hand.
    325 
    326 A reproducer added while its finding is still open will make
    327 `make check-corpus` fail, which is intended -- that is what a regression
    328 test for an unfixed bug does.  `contrib/oss-fuzz/make_seed_corpus.sh`
    329 skips such a file so that ClusterFuzz does not spend every run
    330 rediscovering it; the rule it uses is whether `patches/$ID.diff` exists.