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.