commit d0270f583a5df58371f7338b668811c9cc8056f7 parent 1b7b319d04718e1b53b7caf3f8a55d7bd129c52f Author: Christian Grothoff <christian@grothoff.org> Date: Tue, 28 Jul 2026 16:56:26 +0200 expand fuzzer coverage Diffstat:
70 files changed, 2066 insertions(+), 115 deletions(-)
diff --git a/contrib/ci/jobs/4-fuzz/2-fuzz.sh b/contrib/ci/jobs/4-fuzz/2-fuzz.sh @@ -23,7 +23,13 @@ make -C src/fuzz check \ MHD_FUZZ_ITERATIONS="${FUZZ_ITERATIONS}" \ MHD_FUZZ_SEED="${FUZZ_SEED}" -# Replay the whole committed seed corpus - including -# corpus/known-findings/ - through every harness. This is the part that -# proves a fixed crash stays fixed, and it is cheap. +# Replay the whole committed seed corpus through every harness. This is +# the part that proves a fixed crash stays fixed, and it is cheap. +# +# corpus/known-findings/ is deliberately NOT included: check-corpus does +# not recurse, and the reproducers of the findings that are still open +# (src/fuzz/README section 6, patches/) are expected to fail on a build +# with --enable-asserts. Replay those by hand when working on a fix: +# for f in src/fuzz/corpus/known-findings/*.bin ; do +# ./src/fuzz/fuzz_request --file="$f" ; done make -C src/fuzz check-corpus diff --git a/contrib/oss-fuzz/README b/contrib/oss-fuzz/README @@ -69,7 +69,12 @@ the corpus packager live here and not in the OSS-Fuzz repository. fuzz_request a real struct MHD_Daemon driven over a socketpair; consumes daemon options *and* - stream segmentation from the input + stream segmentation from the input. Bytes + 4-9 (see src/fuzz/README section 2.2.1) + additionally select the response + constructor, the authentication entry point, + the event-loop API, and whether the + connection is suspended or upgraded fuzz_str the mhd_str.c primitives with exactly-sized output buffers fuzz_auth_header MHD_get_rq_dauth_params_() / @@ -93,8 +98,8 @@ range (-3 and 0), which is what a fuzzing service should explore. `max_len` in the `.options` files is set to the point beyond which the harness ignores the extra bytes, so libFuzzer does not waste its budget: - fuzz_request 8192 4 configuration bytes + length-prefixed send - segments (2-byte header, payload <= 0x3FFF, + fuzz_request 8192 10 configuration bytes + length-prefixed + send segments (2-byte header, payload <= 0x3FFF, at most 96 segments over at most 8 connections). 8 KiB comfortably holds the two-request %%NONCE%% digest handshake, a @@ -137,7 +142,7 @@ actionable. ever stops being wired into configure.ac. --enable-asserts keeps mhd_assert() alive. Assertions reachable from network input - are remote aborts; findings K1-K6 in `src/fuzz/README` section 6 + are remote aborts; findings K1-K7 in `src/fuzz/README` section 6 are all of that kind and are invisible without this. --disable-https the harnesses never speak TLS (they hand MHD an already-connected @@ -328,13 +333,21 @@ harnesses, because byte 0 selects a different thing in each, so each zip gets only its own prefix. `src/fuzz/corpus/README` is documentation and is excluded. -`src/fuzz/corpus/known-findings/K1..K6*.bin` are byte-exact reproducers -for the six findings documented in `src/fuzz/README` section 6. They are -all `fuzz_request` inputs and are added to that target's seed corpus +`src/fuzz/corpus/known-findings/K*.bin` are byte-exact reproducers for +the findings documented in `src/fuzz/README` section 6. They are all +`fuzz_request` inputs and are added to that target's seed corpus (prefixed `known-finding-`), which is what turns them into permanent regression tests: ClusterFuzz keeps every seed in the corpus and replays it on every run. +Reproducers of findings that are still *open* are skipped. A finding is +open exactly when `patches/$ID.diff` exists (see `patches/README`), and +its reproducer crashes the target by construction, so shipping it would +make every ClusterFuzz run open by rediscovering a bug that is already +written down. Committing the fix means deleting the diff, and that +alone promotes the reproducer to a shipped regression seed. At the time +of writing this skips K7. + Regenerating the corpus from the harnesses' built-in seeds: make -C src/fuzz refresh-corpus # ./fuzz_<name> --write-corpus=corpus diff --git a/contrib/oss-fuzz/build.sh b/contrib/oss-fuzz/build.sh @@ -188,7 +188,7 @@ fi # --enable-asserts # keeps mhd_assert() alive. Assertions on attacker-reachable paths # are exactly what this campaign is meant to find; without them -# findings K1-K6 (src/fuzz/README section 6) are invisible. +# findings K1-K7 (src/fuzz/README section 6) are invisible. # --disable-https # deliberate. The harnesses never speak TLS: they hand MHD an # already-connected AF_UNIX socketpair via MHD_add_connection() and diff --git a/contrib/oss-fuzz/dicts/fuzz_request.dict b/contrib/oss-fuzz/dicts/fuzz_request.dict @@ -141,3 +141,16 @@ kv="a=b" kv_amp="&" kv_plus="+" kv_noeq="a" + +# HTTP "Upgrade" -- the extended configuration (byte 7 bit 0x04) only +# answers 101 for a request that really asks to upgrade, so without +# these tokens MHD_create_response_for_upgrade(), the upgrade handler +# and MHD_upgrade_action() are statistically unreachable. +upgrade_hdr="Upgrade: " +upgrade_conn="Connection: Upgrade\x0d\x0a" +upgrade_conn_lc="connection: upgrade\x0d\x0a" +upgrade_websocket="Upgrade: websocket\x0d\x0a" +upgrade_h2c="Upgrade: h2c\x0d\x0a" +upgrade_proto="fuzz-protocol" +upgrade_both="Connection: keep-alive, Upgrade\x0d\x0a" +conn_close="Connection: close\x0d\x0a" diff --git a/contrib/oss-fuzz/make_seed_corpus.sh b/contrib/oss-fuzz/make_seed_corpus.sh @@ -16,12 +16,14 @@ # harness the seed belongs to. Inputs are NOT # interchangeable between harnesses: byte 0 of # every harness input selects a different thing. -# known-findings/K*.bin byte-exact reproducers for findings K1-K6 -# (src/fuzz/README section 6). All of them are +# known-findings/K*.bin byte-exact reproducers for the findings in +# src/fuzz/README section 6. All of them are # fuzz_request inputs, so they go into that # harness' seed corpus, where OSS-Fuzz will keep # re-running them forever - i.e. they become -# permanent regression tests. +# permanent regression tests. Reproducers of +# findings that are still open are skipped; see +# the loop below. # README documentation, not an input; excluded. # # The corpus itself is regenerated from the harnesses' built-in seeds with @@ -35,6 +37,7 @@ OUTDIR="${2:-${OUT:-$(pwd)/out}}" CORPUS="${SRCDIR}/src/fuzz/corpus" FINDINGS="${CORPUS}/known-findings" +PATCHES="${SRCDIR}/patches" FUZZERS="fuzz_request fuzz_str fuzz_auth_header fuzz_postprocessor" @@ -59,10 +62,25 @@ for fuzzer in ${FUZZERS}; do n=$((n + 1)) done - # The K1-K6 reproducers are fuzz_request inputs. + # The K* reproducers are fuzz_request inputs. + # + # Only the ones whose defect is already fixed are shipped. A finding + # that is still open has an unapplied fix in patches/$ID.diff (see + # patches/README), and its reproducer, by construction, crashes the + # target: shipping it would make every ClusterFuzz run start by + # rediscovering a bug that is already written down, and bury the + # findings that are actually new. Deleting the diff -- which is what + # committing the fix should do -- promotes the reproducer to a + # permanent regression seed here, with no further edit. if [ "${fuzzer}" = "fuzz_request" ] && [ -d "${FINDINGS}" ]; then for f in "${FINDINGS}"/*.bin; do [ -f "${f}" ] || continue + id="$(basename "${f}" | sed -n 's/^\(K[0-9]*\).*/\1/p')" + if [ -n "${id}" ] && [ -f "${PATCHES}/${id}.diff" ]; then + echo " skipping ${fuzzer} seed $(basename "${f}"): ${id} is open" \ + "(${PATCHES}/${id}.diff)" + continue + fi cp "${f}" "${dir}/known-finding-$(basename "${f}")" n=$((n + 1)) done diff --git a/src/fuzz/BUILD-INTEGRATION.md b/src/fuzz/BUILD-INTEGRATION.md @@ -190,7 +190,9 @@ make -C src/fuzz check-corpus 50000 iterations per harness, roughly 3 seconds in total under ASAN+UBSAN. The findings K1-K6 of `README` section 6 are all fixed on master, so the full range is clean; their reproducers stay in - `corpus/known-findings/` as regressions. + `corpus/known-findings/` as regressions. K7 is open, and its + reproducer lives there too — `check-corpus` does not recurse into that + directory, which is what keeps `make check` green while it is. --- diff --git a/src/fuzz/README b/src/fuzz/README @@ -78,8 +78,11 @@ MHD_get_connection_info() see something sane. 0x04 run the request body through MHD_post_process() 0x08 iterate MHD_get_connection_values() over headers, GET arguments, cookies and footers + 0x10 unused (see 2.2.1) 0x20 reply with a larger, copied response body + (only meaningful for response kind 0) 0x40 reply 403 instead of 200 + 0x80 unused byte 2 low nibble: MHD_OPTION_CLIENT_DISCIPLINE_LVL selector (index into {-3,-2,-1,0,1,2}); high nibble: reserved for MHD_OPTION_SERVER_INSANITY @@ -87,7 +90,8 @@ MHD_get_connection_info() see something sane. byte 3 digest configuration: bits 0-1 select the algorithm of the 401 challenge {SHA-256, MD5, SHA-512-256, SHA-256}, bit 2 selects the QOP, bits 4-5 select MHD_OPTION_NONCE_NC_SIZE - byte 4.. a sequence of *send segments*. Each segment starts with a + byte 4-9 the API-selection block, see 2.2.1 + byte 10. a sequence of *send segments*. Each segment starts with a little-endian 16 bit header: (op << 14) | length length <= 0x3FFF @@ -105,6 +109,112 @@ that the fuzzer can move a split point without having to invent an escaping scheme. Splitting matters: MHD's parser is incremental and several bugs only appear for particular split points. +An input shorter than ten bytes is rejected: the configuration block is +mandatory, and an input that short has no segment stream either. + +2.2.1 The API-selection block +----------------------------- + +Bytes 4-9 pick which parts of the public API the iteration touches. +They are always present. Bytes 0-3 alone decide how MHD *parses* the +request; these six decide which of the response constructors, which +authentication entry point, which event loop, and which introspection +calls run against the parsed result. + +The all-zero setting is the plainest one -- a static two byte buffer +response, MHD_run() as the event loop, no suspend, no upgrade, no extra +introspection -- so zeroing bytes 4-9 of any input reduces it to the +request-parsing-only behaviour that the harness had before these bytes +existed. + +These bytes were gated behind bit 0x10 of byte 1 while they were being +brought up, so that the corpus predating them kept its byte-exact +meaning. The gate is gone; bit 0x10 is left unused rather than +reassigned, so that a corpus file written while it was a gate cannot +silently change meaning. The reproducers in known-findings/ that predate +the change (K1-K6, seven files) were migrated by inserting six zero bytes +at offset 4 -- the identity transformation, since the all-zero block is +the old behaviour -- and all seven still drive MHD along their recorded +paths, verified by comparing the --verbose daemon, handler, body and +challenge counts before and after. + + byte 4 response construction + bits 0-3 which constructor to use: + 0 buffer_static 1 buffer_copy 2 empty + 3 buffer/PERSISTENT 4 buffer/MUST_FREE 5 buffer/MUST_COPY + 6 buffer_with_free_callback 7 data (copy) + 8 data (free) 9 callback, known length + 10 callback, MHD_SIZE_UNKNOWN (chunked reply) + 11 fd 12 fd_at_offset 13 fd64 + 14 pipe 15 iovec + bits 4-5 number of response headers to add (0-3) + bit 6 also add a response footer (a chunked trailer) + bit 7 exercise MHD_get_response_header(), + MHD_get_response_headers(), + MHD_del_response_header() and + MHD_set_response_options() + byte 5 authentication entry point + bits 0-3 0 check3 (as before) 1 check + 2 check2 3 check_digest + 4 check_digest2 5 check_digest3 + 6 get_username 7 get_username3 + 8 get_request_info3 9 as 0 + bits 4-5 challenge variant: 0 queue_auth_required_response3, + 1 queue_auth_fail_response, + 2 queue_auth_fail_response2, + 3 the basic-auth pair + bit 6 use the v1 MHD_basic_auth_get_username_password() + bit 7 also call the connection-less digest helpers + byte 6 event loop and introspection + bits 0-1 0 MHD_run(), 1 MHD_get_fdset()+run_from_select(), + 2 and 3 the *2 variants + bit 2 query all four MHD_get_timeout*() forms + bit 3 MHD_lookup_connection_value(), + MHD_lookup_connection_value_n(), + MHD_get_connection_URI_path_n() + bit 4 MHD_set_connection_value() + bit 5 MHD_get_connection_info() / MHD_get_daemon_info() + bit 6 MHD_set_connection_option() + bit 7 MHD_quiesce_daemon() before stopping + byte 7 suspend and upgrade + bits 0-1 0 none, 1 suspend+resume in the handler, + 2 suspend and resume from the pump loop, + 3 as 1 + bit 2 allow HTTP "Upgrade" (only acted on when the + request really asks for one, see below) + bits 3-4 which MHD_upgrade_action() to issue first + byte 8 seed picking the response header names and values, and the + MHD_RF_* flags for MHD_set_response_options() + byte 9 seed for the content-reader callback: body length, block + size, and whether it fails part way through + +Two constraints on the harness are worth stating, because both are +application-contract requirements rather than things worth fuzzing, and +violating either makes MHD abort on input that is perfectly legal: + + * the connection is only suspended when the handler is about to return + MHD_YES. Returning MHD_NO asks MHD to terminate the connection, and + terminating one that the same callback just suspended trips + mhd_assert (! connection->suspended) in MHD_connection_close_(); + + * a 101 response is only queued for a request that is actually an + upgrade request (HTTP/1.1, an "Upgrade" header, and a "Connection" + header naming the upgrade token and not "close"). All three come + off the wire, so the path stays attacker-driven. + + Unlike the suspend rule, this one is not sufficient, and is not + supposed to be: it is exactly the check a real application can + perform, and a request that passes it can still abort MHD. That is + finding K7 in section 6. Do not tighten it further to keep the + suite green -- the point is that an application cannot do better. + +The message-framing headers (Content-Length, Transfer-Encoding) are +deliberately absent from the response-header table: MHD generates them +itself from the response object, so an application that also sets them +by hand is lying to the library about its own body. +MHD_RF_INSANITY_HEADER_CONTENT_LENGTH is the sanctioned way to explore +that corner and is reachable through bit 7 of byte 4. + 2.3 The %%NONCE%% placeholder ----------------------------- @@ -416,14 +526,106 @@ K6 digestauth.c:860 check_nonce_nc(): The generator needs ~150k iterations to hit it on its own. Repro: corpus/known-findings/K6-nonce-length-collision.bin +K7 connection.c:2596 build_header_response(): + mhd_assert ((NULL == r->upgrade_handler) || + (MHD_CONN_MUST_UPGRADE == c->keepalive)) + Open. Driven entirely from the wire, with default daemon options. + + While parsing the request headers, connection.c sets + c->keepalive = MHD_CONN_MUST_CLOSE + in three places for requests whose message framing it distrusts: + + 4988 two "Content-Length" headers with different values + (client discipline -3 only) + 5012 "Transfer-Encoding" on an HTTP/1.0 request + (client discipline <= 0) + 5051 "Content-Length" *and* "Transfer-Encoding: chunked" in the + same request -- no discipline gate at all, so this one + fires with the default configuration + + In every case the request is then handed to the access handler as a + normal request. If the handler answers 101 with a response from + MHD_create_response_for_upgrade(), MHD_queue_response() accepts it: + it checks MHD_ALLOW_UPGRADE, the status code, the "Connection" + header and the HTTP version, but not whether the connection can + still be kept open. keepalive_possible() then returns + MHD_CONN_MUST_CLOSE from its very first test, which is placed + *before* the upgrade branch, and the assertion above fires while the + reply header is being built. + + The application cannot defend itself. The harness checks the + request the way an application would -- HTTP/1.1, an "Upgrade" + header, and a "Connection" header naming the upgrade token and not + "close" -- and the reproducer satisfies all three. MHD's decision + is not exposed through any public accessor. + + It needs --enable-asserts and an application that answers upgrade + requests, so it is a robustness defect rather than a memory-safety + one; but it is reachable from a single well-formed request against a + default-configured server, which is a good deal worse than the + "application misuse" it first looked like. + + Fix: patches/K7.diff, which rejects the response in + MHD_queue_response() next to the other upgrade preconditions, so the + application gets MHD_NO instead of an abort. + Repro: corpus/known-findings/K7-upgrade-after-must-close.bin + GET / HTTP/1.1 + Host: x + Connection: Upgrade + Upgrade: fuzz-protocol + Content-Length: 0 + Transfer-Encoding: chunked + (then "0\r\n\r\n") + +K8 digestauth.c MHD_digest_auth_check_digest2(): + MHD_PANIC ("Wrong 'malgo3' value, only one base hashing + algorithm ... must be specified, API violation") + Open. An API-contract defect rather than a parsing bug: unlike K7 + the offending argument comes from the application, not the network. + + MHD_DIGEST_ALG_AUTO is a documented member of + enum MHD_DigestAuthAlgorithm, and the `algo` parameter of + MHD_digest_auth_check_digest2() is documented only as "digest + algorithms allowed for verification". But the function maps AUTO to + MHD_DIGEST_AUTH_MULT_ALGO3_ANY_NON_SESSION and forwards it to + MHD_digest_auth_check_digest3(), which requires *exactly one* base + hashing algorithm because the caller supplies a pre-computed digest + of one specific length -- and aborts through MHD_PANIC() when more + than one is named. + + So MHD_digest_auth_check_digest2 (..., MHD_DIGEST_ALG_AUTO) + deterministically kills the process. The same is not true of + MHD_digest_auth_check2(), which takes a password rather than a + digest and whose AUTO handling is fine. + + Unlike K7 this is entirely under the application's control -- no + network input is involved -- so it is a usability trap rather than a + robustness bug: AUTO simply cannot be used with the + pre-computed-digest entry points, and no mapping can rescue it, + because the digest length does not identify the algorithm (SHA-256 + and SHA-512/256 digests are both 32 bytes). + + Fix: patches/K8.diff, which documents the restriction in + microhttpd.h and returns MHD_NO instead of aborting. + Repro: none committed. The argument is the application's, so the + harness never passes AUTO there (see run_digest_check() in + fuzz_request.c); to see it, change the MHD_DIGEST_ALG_* argument of + the variant-4 call to MHD_DIGEST_ALG_AUTO and replay + corpus/fuzz_request-36.bin. + Status ------ K1-K5 are fixed on master by commits 300a2ab0, 68c83f22, e04eb218, -0b750975 and 6fcdfd43 respectively; K6 by f438804c. All seven +0b750975 and 6fcdfd43 respectively; K6 by f438804c. Their seven reproducers in `corpus/known-findings/` therefore replay clean, and `make check-corpus` asserts exactly that. +K7 and K8 are open, and their fixes are unapplied diffs in +`../../patches/`. K7 has a reproducer, which aborts on an +`--enable-asserts` build and passes with patches/K7.diff applied; K8 has +none, because its trigger is an argument the application chooses. + The `make check` defaults in Makefile.am MHD_FUZZ_ITERATIONS = 50000 @@ -494,9 +696,10 @@ Ready to go: see `../../contrib/oss-fuzz/` and its README. That directory holds the OSS-Fuzz `build.sh`, `project.yaml` and `Dockerfile`, per-harness dictionaries (`dicts/fuzz_*.dict`), per-harness `.options` files (`max_len`, `dict`) and `make_seed_corpus.sh`, which packages -`corpus/` — including `corpus/known-findings/`, so that K1-K6 become -permanent regressions — into the `<fuzzer>_seed_corpus.zip` files -OSS-Fuzz expects. +`corpus/` — including the `corpus/known-findings/` reproducers of the +findings that are already *fixed*, so that they become permanent +regressions — into the `<fuzzer>_seed_corpus.zip` files OSS-Fuzz +expects. `build.sh` configures out of tree with diff --git a/src/fuzz/corpus/README b/src/fuzz/corpus/README @@ -20,35 +20,85 @@ Files belonging to another harness are simply uninteresting inputs for the harness that reads them, so pointing every harness at the whole directory is fine and gives some extra cross-pollination. +The set is kept minimal: every seed here adds coverage that no other +seed provides. After changing a seed table, re-check that with + + <target> -merge=1 <empty dir> <this directory> + +from an OSS-Fuzz style build (contrib/oss-fuzz/build.sh); a seed the +merge does not copy across is redundant and should be deleted from the +table rather than left in the corpus. Note that fuzz_request is +mildly non-deterministic -- MHD timestamps its nonces and its +connection timeouts -- so a single merge decision worth ~0.1% of the +regions is noise; only drop a seed that several independent merges +agree on. + fuzz_request seeds ------------------ - 00 plain GET / with one header - 01 content-length-body body oracle, Content-Length - 02 chunked-with-extensions body oracle, chunk extensions + 00 content-length-body body oracle, Content-Length + 01 chunked-with-extensions body oracle, chunk extensions -> chunk-extension CRLF bug - 03 chunked-split body oracle, chunk boundary split + 02 chunked-split body oracle, chunk boundary split across two send() calls - 04 small-pool-trailing-query-arg 128..512 byte connection pool, no + 03 small-pool-trailing-query-arg 128..512 byte connection pool, no header lines, "?novalue" -> read-buffer shift-back bug - 05 small-pool-trailing-query-arg-2 same with "?a=1&b" - 06 digest-unknown-algorithm algorithm=BOGUS + 04 small-pool-trailing-query-arg-2 same with "?a=1&b" + 05 digest-unknown-algorithm algorithm=BOGUS -> MHD_DIGEST_AUTH_ALGO3_INVALID MHD_PANIC() - 07 digest-overlong-response 401 challenge, then a replay on a + 06 digest-overlong-response 401 challenge, then a replay on a fresh connection with the harvested nonce and a 128 hex digit response= -> stack buffer overflow - 08 digest-userhash userhash=true with a 128 char + 07 digest-userhash userhash=true with a 128 char username - 09 basic-auth Authorization: Basic - 10 multipart-post chunked multipart/form-data - 11 urlencoded-post application/x-www-form-urlencoded - 12 folded-header obs-fold continuation line - 13 pipelined two requests in one segment + 08 basic-auth Authorization: Basic + 09 multipart-post chunked multipart/form-data + 10 urlencoded-post application/x-www-form-urlencoded + 11 folded-header obs-fold continuation line + 12 pipelined two requests in one segment + +Seeds 00-12 leave the API-selection block (bytes 4-9, see section 2.2.1 +of ../README) zero and only drive the request parser. Seeds 13-39 each +switch on one area of it; without them those entry points are reachable +only by guessing six configuration bytes. + + 13 ext-callback-chunked MHD_SIZE_UNKNOWN content reader, + response headers and a trailer + 14 ext-callback-error known-length reader that fails + part way through + 15 ext-fd-response MHD_create_response_from_fd() + 16 ext-fd-at-offset-response ..._from_fd_at_offset() + 17 ext-pipe-response ..._from_pipe() + 18 ext-iovec-response ..._from_iovec() + 19 ext-empty-response-hdrapi ..._empty() plus get/del header + and MHD_set_response_options() + 20 ext-digest-check-digest3 pre-computed userdigest path + 21 ext-digest-v1-wrappers MHD_digest_auth_check() + 22 ext-digest-request-info MHD_digest_auth_get_request_info3() + 23 ext-external-event-loop MHD_get_fdset2()/run_from_select2() + plus the timeout accessors + 24 ext-external-event-loop-v1 the v1 fdset/run_from_select pair + 25 ext-connection-api every introspection accessor + 26 ext-suspend-resume suspend, resume from the pump loop + 27 ext-suspend-two-connections two connections parked at once + 28 ext-upgrade 101 + MHD_upgrade_action() + 29 ext-buffer-persistent buffer/MHD_RESPMEM_PERSISTENT + 30 ext-buffer-free-callback ..._with_free_callback() + 31 ext-from-data MHD_create_response_from_data() + 32 ext-fd64-response ..._from_fd64() + 33 ext-basic-auth-v1 MHD_basic_auth_get_username_password() + 34 ext-basic-auth-challenge MHD_queue_basic_auth_fail_response() + 35 ext-basic-auth-challenge-utf8 ..._response3() with UTF-8 charset + 36 ext-digest-check-digest-v1 MHD_digest_auth_check_digest() + 37 ext-digest-check-digest2 ..._digest2() (never with + MHD_DIGEST_ALG_AUTO, see K8) + 38 ext-digest-get-username-v1 MHD_digest_auth_get_username() + 39 ext-digest-get-username3 ..._username3() fuzz_str seeds -------------- @@ -64,7 +114,10 @@ fuzz_auth_header seeds Well-formed and broken Digest parameter lists (unknown algorithm, extended `username*` notation, unterminated quoted strings, empty - parameter list, over-long values) plus Basic `token68` variants. + parameter list, over-long values) plus Basic `token68` variants, and + -- with bit 0x02 of byte 0 -- the connection-less digest helpers at + several algorithms and several deliberately undersized output + buffers. fuzz_postprocessor seeds ------------------------ @@ -78,34 +131,42 @@ fuzz_postprocessor seeds known-findings/ --------------- -Reproducers for the issues that this suite found in MHD 1.0.7 itself; -see section 6 of ../README. K1-K5 are fixed by ../../../patches/B17, -B19, B20, B21 and B22; K6 is handled separately and is still open. They -are deliberately kept out of the main corpus directory so that -`check-corpus` stays green on an unpatched tree. Replay one with +Reproducers for the issues that this suite found in MHD itself; see +section 6 of ../README, which records the status of each. K1-K6 are +fixed on master and therefore replay clean; K7 is open and *aborts* on a +build configured with --enable-asserts. + +They are kept out of the main corpus directory for exactly that reason: +`check-corpus` does not recurse, so it stays green while an open finding +still has a live reproducer here. Replay one with ./fuzz_request --file=src/fuzz/corpus/known-findings/K1-digest-empty-realm.bin K1-digest-empty-realm.bin digestauth.c is_param_equal() mhd_assert (0 != param->value.len) - -> patches/B17.diff K2-chunkext-no-space.bin connection.c handle_recv_no_space() - -> patches/B19.diff K3-chunkext-stop-with-error.bin connection.c transmit_error_response_len() - -> patches/B20.diff K4a-wsp-first-header.bin connection.c get_req_header(), CLIENT_DISCIPLINE_LVL <= -1 - -> patches/B21.diff K4b-empty-header-name.bin connection.c get_req_header(), CLIENT_DISCIPLINE_LVL <= -2 - -> patches/B21.diff K5-bare-cr-keep.bin connection.c get_req_header(), CLIENT_DISCIPLINE_LVL == -3 - -> patches/B22.diff K6-nonce-length-collision.bin digestauth.c check_nonce_nc() mhd_assert (0 == nn->nonce[noncelen]) - -> still open - -Once a patch has landed, move the corresponding file into the main -corpus directory so that `check-corpus` keeps it as a regression seed. + K7-upgrade-after-must-close.bin connection.c build_header_response() + mhd_assert (upgrade -> MUST_UPGRADE) + *** still open, see ../../../patches *** + +These files are *not* regenerated by `--write-corpus`; they are edited by +hand. When the input format changes they have to be migrated, and the +migration has to be checked -- replay each one with `--verbose` before +and after and confirm the daemon, handler, body and challenge counts are +unchanged. The last such change made the API-selection block +unconditional, which is six zero bytes inserted at offset 4 for all +seven. + +K8 has no reproducer here: its trigger is an argument the application +chooses, not network input, so the harness never performs it. See +section 6 of ../README for how to reach it by hand. diff --git a/src/fuzz/corpus/fuzz_auth_header-09.bin b/src/fuzz/corpus/fuzz_auth_header-09.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_auth_header-10.bin b/src/fuzz/corpus/fuzz_auth_header-10.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_auth_header-11.bin b/src/fuzz/corpus/fuzz_auth_header-11.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_auth_header-12.bin b/src/fuzz/corpus/fuzz_auth_header-12.bin @@ -1 +1 @@ -Basic dXNlcjpwYXNz -\ No newline at end of file +Basic +\ No newline at end of file diff --git a/src/fuzz/corpus/fuzz_auth_header-13.bin b/src/fuzz/corpus/fuzz_auth_header-13.bin @@ -1 +1 @@ -Basic -\ No newline at end of file +Basic ==== +\ No newline at end of file diff --git a/src/fuzz/corpus/fuzz_auth_header-14.bin b/src/fuzz/corpus/fuzz_auth_header-14.bin @@ -1 +1 @@ -Basic ==== -\ No newline at end of file +Basic QQ==QQ== +\ No newline at end of file diff --git a/src/fuzz/corpus/fuzz_auth_header-15.bin b/src/fuzz/corpus/fuzz_auth_header-15.bin @@ -1 +1 @@ -Basic QQ==QQ== -\ No newline at end of file +user:TestRealm:pass +\ No newline at end of file diff --git a/src/fuzz/corpus/fuzz_auth_header-16.bin b/src/fuzz/corpus/fuzz_auth_header-16.bin @@ -0,0 +1 @@ +"user:TestRealm:pass +\ No newline at end of file diff --git a/src/fuzz/corpus/fuzz_auth_header-17.bin b/src/fuzz/corpus/fuzz_auth_header-17.bin @@ -0,0 +1 @@ +Buser:TestRealm:pass +\ No newline at end of file diff --git a/src/fuzz/corpus/fuzz_auth_header-18.bin b/src/fuzz/corpus/fuzz_auth_header-18.bin @@ -0,0 +1 @@ +¢user:TestRealm:pass +\ No newline at end of file diff --git a/src/fuzz/corpus/fuzz_auth_header-19.bin b/src/fuzz/corpus/fuzz_auth_header-19.bin @@ -0,0 +1 @@ +:: +\ No newline at end of file diff --git a/src/fuzz/corpus/fuzz_auth_header-20.bin b/src/fuzz/corpus/fuzz_auth_header-20.bin @@ -0,0 +1 @@ + +\ No newline at end of file diff --git a/src/fuzz/corpus/fuzz_auth_header-21.bin b/src/fuzz/corpus/fuzz_auth_header-21.bin @@ -0,0 +1 @@ +Digest username="user", realm="TestRealm" +\ No newline at end of file diff --git a/src/fuzz/corpus/fuzz_request-00.bin b/src/fuzz/corpus/fuzz_request-00.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_request-01.bin b/src/fuzz/corpus/fuzz_request-01.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_request-02.bin b/src/fuzz/corpus/fuzz_request-02.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_request-03.bin b/src/fuzz/corpus/fuzz_request-03.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_request-04.bin b/src/fuzz/corpus/fuzz_request-04.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_request-05.bin b/src/fuzz/corpus/fuzz_request-05.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_request-06.bin b/src/fuzz/corpus/fuzz_request-06.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_request-07.bin b/src/fuzz/corpus/fuzz_request-07.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_request-08.bin b/src/fuzz/corpus/fuzz_request-08.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_request-09.bin b/src/fuzz/corpus/fuzz_request-09.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_request-10.bin b/src/fuzz/corpus/fuzz_request-10.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_request-11.bin b/src/fuzz/corpus/fuzz_request-11.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_request-12.bin b/src/fuzz/corpus/fuzz_request-12.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_request-13.bin b/src/fuzz/corpus/fuzz_request-13.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_request-14.bin b/src/fuzz/corpus/fuzz_request-14.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_request-15.bin b/src/fuzz/corpus/fuzz_request-15.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_request-16.bin b/src/fuzz/corpus/fuzz_request-16.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_request-17.bin b/src/fuzz/corpus/fuzz_request-17.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_request-18.bin b/src/fuzz/corpus/fuzz_request-18.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_request-19.bin b/src/fuzz/corpus/fuzz_request-19.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_request-20.bin b/src/fuzz/corpus/fuzz_request-20.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_request-21.bin b/src/fuzz/corpus/fuzz_request-21.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_request-22.bin b/src/fuzz/corpus/fuzz_request-22.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_request-23.bin b/src/fuzz/corpus/fuzz_request-23.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_request-24.bin b/src/fuzz/corpus/fuzz_request-24.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_request-25.bin b/src/fuzz/corpus/fuzz_request-25.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_request-26.bin b/src/fuzz/corpus/fuzz_request-26.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_request-27.bin b/src/fuzz/corpus/fuzz_request-27.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_request-28.bin b/src/fuzz/corpus/fuzz_request-28.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_request-29.bin b/src/fuzz/corpus/fuzz_request-29.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_request-30.bin b/src/fuzz/corpus/fuzz_request-30.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_request-31.bin b/src/fuzz/corpus/fuzz_request-31.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_request-32.bin b/src/fuzz/corpus/fuzz_request-32.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_request-33.bin b/src/fuzz/corpus/fuzz_request-33.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_request-34.bin b/src/fuzz/corpus/fuzz_request-34.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_request-35.bin b/src/fuzz/corpus/fuzz_request-35.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_request-36.bin b/src/fuzz/corpus/fuzz_request-36.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_request-37.bin b/src/fuzz/corpus/fuzz_request-37.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_request-38.bin b/src/fuzz/corpus/fuzz_request-38.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_request-39.bin b/src/fuzz/corpus/fuzz_request-39.bin Binary files differ. diff --git a/src/fuzz/corpus/known-findings/K1-digest-empty-realm.bin b/src/fuzz/corpus/known-findings/K1-digest-empty-realm.bin Binary files differ. diff --git a/src/fuzz/corpus/known-findings/K2-chunkext-no-space.bin b/src/fuzz/corpus/known-findings/K2-chunkext-no-space.bin Binary files differ. diff --git a/src/fuzz/corpus/known-findings/K3-chunkext-stop-with-error.bin b/src/fuzz/corpus/known-findings/K3-chunkext-stop-with-error.bin Binary files differ. diff --git a/src/fuzz/corpus/known-findings/K4a-wsp-first-header.bin b/src/fuzz/corpus/known-findings/K4a-wsp-first-header.bin Binary files differ. diff --git a/src/fuzz/corpus/known-findings/K4b-empty-header-name.bin b/src/fuzz/corpus/known-findings/K4b-empty-header-name.bin Binary files differ. diff --git a/src/fuzz/corpus/known-findings/K5-bare-cr-keep.bin b/src/fuzz/corpus/known-findings/K5-bare-cr-keep.bin Binary files differ. diff --git a/src/fuzz/corpus/known-findings/K6-nonce-length-collision.bin b/src/fuzz/corpus/known-findings/K6-nonce-length-collision.bin Binary files differ. diff --git a/src/fuzz/fuzz_auth_header.c b/src/fuzz/fuzz_auth_header.c @@ -61,6 +61,28 @@ static const size_t pool_sizes[] = { 256, 512, 1024, 4096, 32768 }; + +/** + * Exactly-sized, NUL terminated copy of @a len bytes of @a src. + * + * "Exactly sized" is the point: the allocation is @a len + 1 bytes and + * not one byte more, so ASAN's redzone sits immediately behind the + * terminator and any read past it is reported. + */ +static char * +fuzz_dup_n (const char *src, + size_t len) +{ + char *r = (char *) malloc (len + 1); + + if (NULL == r) + return NULL; + memcpy (r, src, len); + r[len] = '\0'; + return r; +} + + /** * gen_auth.c logs through MHD_DLOG(), which dereferences the daemon of * the connection, so a real (but idle) daemon is required. It is @@ -296,6 +318,88 @@ LLVMFuzzerTestOneInput (const uint8_t *data, } #endif /* BAUTH_SUPPORT */ +#ifdef DAUTH_SUPPORT + /* The connection-less digest helpers. They belong here rather than + in fuzz_request because they are pure functions of their string + arguments: this harness reaches roughly two orders of magnitude + more executions per second, and -- more importantly -- it can hand + them a username and realm taken straight from the fuzzer instead of + the fixed constants fuzz_request has to use. + + Every output buffer is a heap allocation of *exactly* the size + declared to MHD, and that size is swept down to zero, so a helper + that writes its full digest into a buffer that is too small is + caught immediately by ASAN's redzone rather than silently + corrupting an adjacent object. That is precisely the shape of the + overflow fixed in commit 5a73c1ae. */ + if (0 != (sel & 0x02)) + { + /* Exactly one base hashing algorithm per entry. In particular + MHD_DIGEST_AUTH_ALGO3_INVALID must not appear: every one of these + helpers routes through digest_get_hash_size(), which asserts that + precisely one of MD5 / SHA-256 / SHA-512-256 is named. Passing + INVALID is an API violation on the caller's side, not something + worth fuzzing. */ + static const enum MHD_DigestAuthAlgo3 algo3s[] = { + MHD_DIGEST_AUTH_ALGO3_MD5, + MHD_DIGEST_AUTH_ALGO3_SHA256, + MHD_DIGEST_AUTH_ALGO3_SHA512_256, + MHD_DIGEST_AUTH_ALGO3_MD5_SESSION, + MHD_DIGEST_AUTH_ALGO3_SHA256_SESSION, + MHD_DIGEST_AUTH_ALGO3_SHA512_256_SESSION + }; + enum MHD_DigestAuthAlgo3 a = + algo3s[(sel >> 5) % (sizeof (algo3s) / sizeof (algo3s[0]))]; + size_t hs = MHD_digest_get_hash_size (a); + + /* Split the fuzzer-supplied header value into username / realm / + password. Each gets its OWN exactly-sized allocation rather than + being carved out of `value` in place: that way each string is + followed by its own ASAN redzone, so a helper reading one byte + past the end of the username is reported precisely instead of + quietly running into the realm that would follow it in a shared + buffer. */ + size_t o1 = vlen / 3; + size_t o2 = (2 * vlen) / 3; + char *user = fuzz_dup_n (value, o1); + char *realm = fuzz_dup_n (value + o1, o2 - o1); + char *pass = fuzz_dup_n (value + o2, vlen - o2); + + if ( (NULL != user) && (NULL != realm) && (NULL != pass) ) + { + if ( (0 != hs) && + (hs <= 64) ) + { + size_t claim = hs - (size_t) (data[0] % (unsigned int) (hs + 1u)); + void *bin = malloc (claim); + + if (NULL != bin) + { + (void) MHD_digest_auth_calc_userdigest (a, user, realm, pass, + bin, claim); + (void) MHD_digest_auth_calc_userhash (a, user, realm, bin, claim); + free (bin); + } + } + { + size_t need = (0 != hs) ? (2 * hs + 1) : 1; + size_t claim = need - (size_t) (data[0] % (unsigned int) (need + 1u)); + char *hex = (char *) malloc (claim); + + if (NULL != hex) + { + (void) MHD_digest_auth_calc_userhash_hex (a, user, realm, + hex, claim); + free (hex); + } + } + } + free (user); + free (realm); + free (pass); + } +#endif /* DAUTH_SUPPORT */ + MHD_pool_destroy (pool); free (value); return 0; @@ -411,13 +515,34 @@ static const struct ah_seed ah_seeds[] = { "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef\""), ASEED ("\x00" "Digest"), - ASEED ("\x00" "Digest "), ASEED ("\x00" "Digest ,,,,,"), ASEED ("\x00" "Digest nc=\"ffffffffffffffffffffffff\""), ASEED ("\x01" "Basic dXNlcjpwYXNz"), ASEED ("\x01" "Basic "), ASEED ("\x01" "Basic ===="), - ASEED ("\x01" "Basic QQ==QQ==") + ASEED ("\x01" "Basic QQ==QQ=="), + + /* Bit 0x02 of byte 0 additionally runs the connection-less digest + helpers (MHD_digest_auth_calc_userdigest/_userhash/_userhash_hex) + with the header value split into username / realm / password. Bits + 5-7 pick the algorithm and byte 0 also sets the deliberately + undersized output-buffer length, so the seeds below cover several + algorithms at several buffer sizes. Without a seed here the branch + is only reachable by a lucky bit flip in byte 0. + + The three *_SESSION algorithms are not seeded separately: they hash + with the same primitive as their non-session counterpart, so they + reach no code the entries below do not, and byte 0 is the byte a + mutator flips first anyway. */ + ASEED ("\x02" "user:TestRealm:pass"), + ASEED ("\x22" "user:TestRealm:pass"), + ASEED ("\x42" "user:TestRealm:pass"), + ASEED ("\xa2" "user:TestRealm:pass"), + /* Degenerate splits: empty username, empty realm, empty password. */ + ASEED ("\x02" "::"), + ASEED ("\x02" ""), + /* Both the parser and the helpers in one execution. */ + ASEED ("\x02" "Digest username=\"user\", realm=\"TestRealm\"") }; diff --git a/src/fuzz/fuzz_request.c b/src/fuzz/fuzz_request.c @@ -34,7 +34,19 @@ * byte 1 configuration: handler behaviour bitmask * byte 2 configuration: client discipline / insanity level * byte 3 configuration: digest-auth parameters - * byte 4.. a sequence of send-segments, each introduced by a + * byte 4 response construction: which MHD_create_response_*() + * to use, how many response headers to add, footers, + * header-manipulation API + * byte 5 which authentication entry point to call: the v1/v2 + * compatibility wrappers, the username/request-info + * queries, or the differing fail-response variants + * byte 6 event-loop mode (MHD_run() vs MHD_get_fdset*() + + * MHD_run_from_select*()) and the connection-introspection + * calls + * byte 7 suspend/resume and HTTP "Upgrade" + * byte 8 seed for the generated response header names/values + * byte 9 seed for the content-reader callback behaviour + * byte 10. a sequence of send-segments, each introduced by a * little-endian 16 bit header (op << 14) | length * op 0 send the payload * op 1 the payload is the *expected* decoded request body @@ -42,6 +54,13 @@ * op 2 send the payload and pump the daemon extra rounds * op 3 close the connection, open a fresh one, send * + * All ten configuration bytes are always present; an input shorter + * than that is rejected outright. The all-zero configuration is the + * plainest one -- a static two byte buffer response, MHD_run() as the + * event loop, no suspend, no upgrade, no extra introspection -- so + * zeroing bytes 4-9 of any input reduces it to what the harness did + * before those bytes existed. + * * Splitting the byte stream into explicit segments matters: MHD's * parser is incremental and several past bugs only showed up for * particular split points. @@ -53,12 +72,44 @@ * code that is only executed for a *valid* nonce. */ +/* Must precede every #include: memfd_create() and MFD_CLOEXEC are only + declared by <sys/mman.h> under _GNU_SOURCE, and the OSS-Fuzz build + compiles this file with nothing but -DFUZZ_NO_MAIN (the library gets + _GNU_SOURCE from configure, the hand-compiled harnesses do not). + Without this the fd-backed response kinds silently fall back to a + buffer response and MHD_create_response_from_fd*() is never reached + -- a failure mode with no error message at all. */ +#ifndef _GNU_SOURCE +#define _GNU_SOURCE 1 +#endif + #define FUZZ_HARNESS_NAME "fuzz_request" #include "fuzz_common.h" #include <microhttpd.h> #include <sys/socket.h> #include <netinet/in.h> +#include <sys/select.h> + +/* MHD_create_response_from_data() and MHD_create_response_from_buffer() + with an explicit MHD_ResponseMemoryMode are deprecated but are still + shipped API and are therefore still worth fuzzing; the deprecation + warning would otherwise drown the build output. */ +#if defined(__GNUC__) || defined(__clang__) +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" +#endif + +/* memfd_create() gives a file descriptor backed by anonymous memory, + which is what makes the MHD_create_response_from_fd*() variants + affordable at fuzzing rates: no filesystem is touched. Where it is + unavailable the fd-backed response kinds fall back to a buffer + response (see make_response()). */ +#if defined(__linux__) +#include <sys/mman.h> +#if defined(MFD_CLOEXEC) +#define FUZZ_HAVE_MEMFD 1 +#endif +#endif #define MAX_SEGMENTS 96 #define MAX_CONNECTIONS 8 @@ -66,6 +117,13 @@ #define GEN_BUF_SIZE 8192 #define MAX_EXPECT_BODY 4096 +/** Number of entries in the response-construction table. */ +#define RESP_KIND_COUNT 16 +/** Number of authentication entry points selectable by byte 5. */ +#define DAUTH_VARIANT_COUNT 10 +/** Largest digest MHD_digest_auth_calc_userdigest() can produce. */ +#define MAX_DIGEST_BIN 64 + #define DIGEST_REALM "TestRealm" #define DIGEST_USER "user" #define DIGEST_PASS "pass" @@ -88,6 +146,28 @@ struct fuzz_cfg int do_iterate; int chunked_reply; int error_reply; + + /* ---- configuration bytes 4-9; all zero is the plainest setting ---- */ + unsigned int resp_kind; /**< index into the response constructors */ + unsigned int resp_nhdr; /**< how many response headers to add */ + int resp_footer; /**< also add a response footer/trailer */ + int resp_hdr_api; /**< exercise get/del/set_response_options */ + unsigned int dauth_variant; /**< which digest entry point to call */ + unsigned int fail_variant; /**< which *_fail_response to queue */ + int basic_v1; /**< use the v1 basic-auth getter */ + int calc_helpers; /**< call the userdigest/userhash helpers */ + unsigned int loop_mode; /**< 0 MHD_run, 1..3 external event loop */ + int use_timeouts; /**< query MHD_get_timeout*() while pumping */ + int conn_lookup; /**< MHD_lookup_connection_value*() */ + int conn_setvalue; /**< MHD_set_connection_value() */ + int conn_info; /**< MHD_get_connection_info/daemon_info */ + int conn_option; /**< MHD_set_connection_option() */ + int quiesce; /**< MHD_quiesce_daemon() before stopping */ + unsigned int suspend_mode; /**< 0 none, 1 immediate, 2 deferred */ + int allow_upgrade; /**< MHD_ALLOW_UPGRADE + upgrade response */ + unsigned int upgrade_action; /**< which MHD_upgrade_action() to issue */ + uint8_t hdr_seed; /**< picks response header name/value */ + uint8_t crc_seed; /**< content-reader callback behaviour */ }; static struct fuzz_cfg cfg; @@ -182,6 +262,509 @@ static const char digest_rnd[32] = /* ------------------------------------------------------------------ */ +/* Configuration byte 4: response construction */ +/* ------------------------------------------------------------------ */ + +/* Defined with the access handler further down; decorate_response() + hands it to MHD_get_response_headers(). */ +static enum MHD_Result +kv_iter (void *cls, + enum MHD_ValueKind kind, + const char *key, + const char *value); + +/** Daemon of the current iteration; needed by MHD_get_daemon_info(). */ +static struct MHD_Daemon *cur_daemon; + +/** + * Connections suspended by suspend mode 2, which the pump loop still has + * to resume. An entry is cleared by completed_cb() so that a connection + * MHD has finished with is never resumed afterwards. + * + * This has to be a set, not a single slot: op 3 opens a fresh connection + * without waiting for the previous one to finish, so several connections + * can be parked at the same time. A single slot silently loses all but + * the last, and the forgotten one is still suspended when the iteration + * calls MHD_stop_daemon(), which answers with + * MHD_PANIC ("MHD_stop_daemon() called while we have suspended + * connections") -- a harness bug that looks exactly like an MHD bug. + */ +static struct MHD_Connection *pending_resume[MAX_CONNECTIONS]; + +/** + * Set once the iteration has stopped feeding the daemon and only wants + * to drain it. suspend_maybe() then does nothing, which is what makes + * the flush loop in the teardown provably terminate: without it, + * resuming a connection lets the handler run and park it again, for as + * many rounds as there are pipelined requests still buffered. + */ +static int tearing_down; + + +/** + * Record @a c as suspended. Silently ignores the overflow case: the + * array has one slot per connection the harness can open, so it cannot + * overflow, and a dropped entry would only cost the resume below. + */ +static void +pending_resume_add (struct MHD_Connection *c) +{ + unsigned int i; + + for (i = 0; i < MAX_CONNECTIONS; i++) + { + if (NULL == pending_resume[i]) + { + pending_resume[i] = c; + return; + } + } +} + + +/** Forget @a c without resuming it (it is gone). */ +static void +pending_resume_drop (const struct MHD_Connection *c) +{ + unsigned int i; + + for (i = 0; i < MAX_CONNECTIONS; i++) + if (pending_resume[i] == c) + pending_resume[i] = NULL; +} + + +/** + * Resume everything still parked. + * + * @return non-zero if at least one connection was resumed, so that the + * caller knows the daemon needs another round to act on it + */ +static int +pending_resume_flush (void) +{ + unsigned int i; + int any = 0; + + for (i = 0; i < MAX_CONNECTIONS; i++) + { + struct MHD_Connection *c = pending_resume[i]; + + if (NULL == c) + continue; + /* Clear first: MHD_resume_connection() can run the handler, which + may suspend the very same connection again. */ + pending_resume[i] = NULL; + MHD_resume_connection (c); + any = 1; + } + return any; +} + + +/** Body used by the buffer / fd / pipe / iovec response kinds. */ +static const char resp_body[] = "hello world, this is the response body"; +#define RESP_BODY_LEN (sizeof (resp_body) - 1) + +/** + * Response header names and values, indexed by cfg.hdr_seed. The list + * mixes ordinary headers with ones MHD has to reject or handle + * specially: an empty name, a name carrying a control character, and a + * value containing CRLF -- the classic response-splitting vector, which + * MHD_add_response_header() is supposed to refuse. Exercising that + * validator is the point of this table. + * + * The message-framing headers are deliberately NOT here. MHD generates + * Content-Length and Transfer-Encoding itself from the response object, + * so an application that also sets them by hand is lying to the library + * about its own body: a manual "Transfer-Encoding: chunked" on, say, an + * iovec response sends MHD down the chunked path for a response that + * has neither a content reader nor a flat buffer, and it aborts in + * try_ready_chunked_body(). That is the harness misusing the API + * rather than a defect worth reporting, and leaving it in would bury + * every real finding under the same false report. MHD_RF_INSANITY_- + * HEADER_CONTENT_LENGTH, set through MHD_set_response_options() in + * decorate_response(), is the sanctioned way to explore that corner. + */ +static const char *const hdr_names[] = { + "X-Fuzz", "Content-Type", "Cache-Control", "Accept-Ranges", + /* "\x01" is split from the rest so that the hex escape stops after + one digit instead of swallowing the following "Bad". */ + "Connection", "Set-Cookie", "X-\x01" "Bad", "", "Trailer", "Date", + "X-Fuzz", "Server", "Location", "ETag", "Vary", "Age" +}; + +static const char *const hdr_values[] = { + "1", "text/plain", "no-cache", "bytes", "keep-alive", "a=b", + "x\r\nInjected: yes", "", "X-Fuzz", "Thu, 01 Jan 1970 00:00:00 GMT", + "\x7f", "mhd", "/", "\"tag\"", "*", "0" +}; + +#define HDR_COUNT (sizeof (hdr_names) / sizeof (hdr_names[0])) + + +/** + * State of one MHD_create_response_from_callback() response. Allocated + * per response and released through the free callback that MHD invokes + * from MHD_destroy_response(). + */ +struct crc_state +{ + uint64_t total; /**< bytes to produce before end-of-stream */ + int calls; /**< how often the reader has been called */ + int err_at; /**< fail after this many calls, -1 to never fail */ + unsigned int pattern; +}; + + +static ssize_t +crc_cb (void *cls, + uint64_t pos, + char *buf, + size_t max) +{ + struct crc_state *st = (struct crc_state *) cls; + size_t n; + size_t i; + + st->calls++; + /* Deliberately reachable: an application content reader is allowed to + fail half way through a response body, and how MHD terminates the + connection in that case (especially a chunked one that can no + longer be framed correctly) is worth exercising. */ + if ( (0 <= st->err_at) && + (st->calls > st->err_at) ) + return MHD_CONTENT_READER_END_WITH_ERROR; + if (pos >= st->total) + return MHD_CONTENT_READER_END_OF_STREAM; + n = (size_t) (st->total - pos); + if (n > max) + n = max; + if (0 == n) + return MHD_CONTENT_READER_END_OF_STREAM; + for (i = 0; i < n; i++) + buf[i] = (char) ('a' + (int) ((pos + i + st->pattern) % 26)); + return (ssize_t) n; +} + + +static void +crc_free (void *cls) +{ + free (cls); +} + + +/** + * Handler for an upgraded ("101 Switching Protocols") connection. + * + * @a extra_in holds whatever the client had already pipelined behind + * the request, which is attacker-controlled and therefore read here. + * + * The connection MUST be closed through MHD_upgrade_action(): the + * socket belongs to the application from this point on and + * MHD_stop_daemon() cannot complete while an upgraded connection is + * still outstanding. + */ +static void +upgrade_cb (void *cls, + struct MHD_Connection *connection, + void *req_cls, + const char *extra_in, + size_t extra_in_size, + MHD_socket sock, + struct MHD_UpgradeResponseHandle *urh) +{ + volatile size_t sink = 0; + + (void) cls; + (void) connection; + (void) req_cls; + (void) sock; + if ( (NULL != extra_in) && + (0 != extra_in_size) ) + sink += (size_t) (unsigned char) extra_in[extra_in_size - 1]; + (void) sink; + if (0 != (cfg.upgrade_action & 0x01u)) + (void) MHD_upgrade_action (urh, MHD_UPGRADE_ACTION_CORK_ON); + if (0 != (cfg.upgrade_action & 0x02u)) + (void) MHD_upgrade_action (urh, MHD_UPGRADE_ACTION_CORK_OFF); + (void) MHD_upgrade_action (urh, MHD_UPGRADE_ACTION_CLOSE); +} + + +/** + * A file descriptor holding RESP_BODY_LEN bytes, backed by anonymous + * memory so that no filesystem is involved. + * + * @return -1 if unavailable, in which case the caller falls back to a + * buffer response + */ +static int +make_memfd (void) +{ +#ifdef FUZZ_HAVE_MEMFD + int fd; + + fd = memfd_create ("mhd-fuzz", MFD_CLOEXEC); + if (0 > fd) + return -1; + if (RESP_BODY_LEN != (size_t) write (fd, resp_body, RESP_BODY_LEN)) + { + (void) close (fd); + return -1; + } + return fd; +#else + return -1; +#endif +} + + +/** + * The read end of a pipe holding RESP_BODY_LEN bytes. The body is far + * below the pipe capacity, so the write cannot block. + */ +static int +make_pipe_fd (void) +{ + int pf[2]; + + if (0 != pipe (pf)) + return -1; + if (RESP_BODY_LEN != (size_t) write (pf[1], resp_body, RESP_BODY_LEN)) + { + (void) close (pf[0]); + (void) close (pf[1]); + return -1; + } + (void) close (pf[1]); + return pf[0]; +} + + +/** + * Add response headers, footers and option flags as selected by byte 4 + * and the header seed in byte 8. + * + * Footers are only meaningful for a chunked response -- MHD emits them + * as trailers after the last chunk -- which is why they are worth + * fuzzing together with the callback-based response kinds. + */ +static void +decorate_response (struct MHD_Response *r) +{ + unsigned int i; + + for (i = 0; i < cfg.resp_nhdr; i++) + { + unsigned int k = (cfg.hdr_seed + i) % HDR_COUNT; + + (void) MHD_add_response_header (r, hdr_names[k], hdr_values[k]); + } + if (cfg.resp_footer) + { + unsigned int k = (unsigned int) (cfg.hdr_seed + 1u) % HDR_COUNT; + + (void) MHD_add_response_footer (r, hdr_names[k], hdr_values[k]); + } + if (cfg.resp_hdr_api) + { + unsigned int k = cfg.hdr_seed % HDR_COUNT; + volatile size_t sink = 0; + const char *v; + + v = MHD_get_response_header (r, hdr_names[k]); + if (NULL != v) + sink += strlen (v); + (void) sink; + (void) MHD_get_response_headers (r, &kv_iter, NULL); + (void) MHD_del_response_header (r, hdr_names[k], hdr_values[k]); + (void) MHD_set_response_options (r, + (enum MHD_ResponseFlags) + (((cfg.hdr_seed & 0x01) ? + MHD_RF_HTTP_VERSION_1_0_RESPONSE : 0) + | ((cfg.hdr_seed & 0x02) ? + MHD_RF_SEND_KEEP_ALIVE_HEADER : 0) + | ((cfg.hdr_seed & 0x04) ? + MHD_RF_INSANITY_HEADER_CONTENT_LENGTH + : 0) + | ((cfg.hdr_seed & 0x08) ? + MHD_RF_HEAD_ONLY_RESPONSE : 0)), + MHD_RO_END); + } +} + + +/** + * Build the response body for this iteration. + * + * Every constructor that fails is responsible for nothing: MHD releases + * neither the buffer nor the file descriptor when it returns NULL (see + * MHD_create_response_from_buffer_with_free_callback_cls() and + * MHD_create_response_from_pipe() in response.c), so ownership stays + * here and the resource has to be released explicitly. Getting this + * backwards would produce double frees that look exactly like MHD bugs. + */ +static struct MHD_Response * +make_response (void) +{ + struct MHD_Response *r = NULL; + struct crc_state *st; + void *heap; + int fd; + + switch (cfg.resp_kind) + { + case 0: + /* The two responses the harness used before byte 4 existed. Bit + 0x20 of byte 1 (cfg.chunked_reply) selects between them and has + no other effect, so keeping the pair here is what keeps that bit + meaningful. */ + if (cfg.chunked_reply) + r = MHD_create_response_from_buffer_copy (11, "hello world"); + else + r = MHD_create_response_from_buffer_static (2, "ok"); + break; + case 1: + r = MHD_create_response_from_buffer_copy (RESP_BODY_LEN, resp_body); + break; + case 2: + r = MHD_create_response_empty (MHD_RF_NONE); + break; + case 3: + r = MHD_create_response_from_buffer (RESP_BODY_LEN, + (void *) (intptr_t) resp_body, + MHD_RESPMEM_PERSISTENT); + break; + case 4: + heap = malloc (RESP_BODY_LEN); + if (NULL == heap) + return NULL; + memcpy (heap, resp_body, RESP_BODY_LEN); + r = MHD_create_response_from_buffer (RESP_BODY_LEN, + heap, + MHD_RESPMEM_MUST_FREE); + if (NULL == r) + free (heap); + break; + case 5: + r = MHD_create_response_from_buffer (RESP_BODY_LEN, + (void *) (intptr_t) resp_body, + MHD_RESPMEM_MUST_COPY); + break; + case 6: + heap = malloc (RESP_BODY_LEN); + if (NULL == heap) + return NULL; + memcpy (heap, resp_body, RESP_BODY_LEN); + r = MHD_create_response_from_buffer_with_free_callback (RESP_BODY_LEN, + heap, + &free); + if (NULL == r) + free (heap); + break; + case 7: + r = MHD_create_response_from_data (RESP_BODY_LEN, + (void *) (intptr_t) resp_body, + 0 /* must_free */, + 1 /* must_copy */); + break; + case 8: + heap = malloc (RESP_BODY_LEN); + if (NULL == heap) + return NULL; + memcpy (heap, resp_body, RESP_BODY_LEN); + r = MHD_create_response_from_data (RESP_BODY_LEN, + heap, + 1 /* must_free */, + 0 /* must_copy */); + if (NULL == r) + free (heap); + break; + case 9: + case 10: + st = (struct crc_state *) calloc (1, sizeof (struct crc_state)); + if (NULL == st) + return NULL; + st->total = (uint64_t) (1u + (cfg.crc_seed & 0x7Fu)); + st->pattern = cfg.crc_seed; + /* Injecting a reader error is only interesting some of the time; + otherwise the body would rarely complete. */ + st->err_at = (0 != (cfg.crc_seed & 0x80u)) + ? (int) (1u + (cfg.crc_seed & 0x03u)) + : -1; + r = MHD_create_response_from_callback ( + (9 == cfg.resp_kind) ? st->total : MHD_SIZE_UNKNOWN, + (size_t) (16u + (cfg.crc_seed & 0x3Fu)), + &crc_cb, + st, + &crc_free); + if (NULL == r) + free (st); + break; + case 11: + case 12: + case 13: + fd = make_memfd (); + if (0 > fd) + { + r = MHD_create_response_from_buffer_static (2, "ok"); + break; + } + if (11 == cfg.resp_kind) + r = MHD_create_response_from_fd (RESP_BODY_LEN, fd); + else if (12 == cfg.resp_kind) + /* Parenthesised so that the deprecated *function* is called and + not the same-named macro that redirects to the 64 bit variant; + reaching the v1 entry point is the whole point here. */ + r = (MHD_create_response_from_fd_at_offset) ( + RESP_BODY_LEN / 2, + fd, + (off_t) (RESP_BODY_LEN / 4)); + else + r = MHD_create_response_from_fd64 ((uint64_t) RESP_BODY_LEN, fd); + if (NULL == r) + (void) close (fd); + break; + case 14: + fd = make_pipe_fd (); + if (0 > fd) + { + r = MHD_create_response_from_buffer_static (2, "ok"); + break; + } + r = MHD_create_response_from_pipe (fd); + if (NULL == r) + (void) close (fd); + break; + case 15: + default: + { + static struct MHD_IoVec iov[3]; + + /* Static data, so no free callback is needed; MHD copies the + array itself (see the documentation of the @a iov parameter). */ + iov[0].iov_base = resp_body; + iov[0].iov_len = RESP_BODY_LEN / 2; + iov[1].iov_base = resp_body + (RESP_BODY_LEN / 2); + iov[1].iov_len = RESP_BODY_LEN - (RESP_BODY_LEN / 2); + iov[2].iov_base = resp_body; + iov[2].iov_len = (size_t) (cfg.crc_seed % (RESP_BODY_LEN + 1)); + r = MHD_create_response_from_iovec (iov, + (0 != iov[2].iov_len) ? 3u : 2u, + NULL, + NULL); + } + break; + } + if (NULL != r) + decorate_response (r); + return r; +} + + +/* ------------------------------------------------------------------ */ /* Access handler */ /* ------------------------------------------------------------------ */ @@ -212,6 +795,33 @@ kv_iter (void *cls, } +/** + * Length-aware counterpart of kv_iter(), for + * MHD_get_connection_values_n(). Unlike the NUL-terminated iterator it + * is handed explicit sizes, so it reads exactly what MHD says is there + * rather than trusting a terminator. + */ +static enum MHD_Result +kv_iter_n (void *cls, + enum MHD_ValueKind kind, + const char *key, + size_t key_size, + const char *value, + size_t value_size) +{ + volatile size_t sink = 0; + + (void) cls; + (void) kind; + if ( (NULL != key) && (0 != key_size) ) + sink += (size_t) (unsigned char) key[key_size - 1]; + if ( (NULL != value) && (0 != value_size) ) + sink += (size_t) (unsigned char) value[value_size - 1]; + (void) sink; + return MHD_YES; +} + + static enum MHD_Result post_iter (void *cls, enum MHD_ValueKind kind, @@ -269,6 +879,483 @@ oracle_check_body (struct hstate *hs, } +/* ------------------------------------------------------------------ */ +/* Configuration byte 5: authentication entry points */ +/* ------------------------------------------------------------------ */ + +static const enum MHD_DigestAuthAlgorithm algo1_tbl[] = { + MHD_DIGEST_ALG_AUTO, + MHD_DIGEST_ALG_MD5, + MHD_DIGEST_ALG_SHA256, + MHD_DIGEST_ALG_AUTO +}; + +static const enum MHD_DigestAuthAlgo3 algo3_tbl[] = { + MHD_DIGEST_AUTH_ALGO3_MD5, + MHD_DIGEST_AUTH_ALGO3_SHA256, + MHD_DIGEST_AUTH_ALGO3_SHA512_256, + MHD_DIGEST_AUTH_ALGO3_MD5_SESSION, + MHD_DIGEST_AUTH_ALGO3_SHA256_SESSION, + MHD_DIGEST_AUTH_ALGO3_SHA512_256_SESSION, + MHD_DIGEST_AUTH_ALGO3_MD5, + MHD_DIGEST_AUTH_ALGO3_SHA256 +}; + +/** Outcome of run_digest_check(). */ +#define DAUTH_CHALLENGE 0 +#define DAUTH_PASSED 1 +#define DAUTH_STALE 2 + + +/** + * Exercise the digest helpers that take no connection: + * MHD_digest_get_hash_size(), MHD_digest_auth_calc_userdigest(), + * MHD_digest_auth_calc_userhash() and + * MHD_digest_auth_calc_userhash_hex(). + * + * The output buffers are heap allocations of *exactly* the size that is + * passed to MHD, and that size is deliberately varied down to zero. A + * correct implementation must reject every buffer that is too small; if + * it instead writes the full digest, ASAN's redzone catches it + * immediately. This is the shape of the bug fixed in commit 5a73c1ae, + * where an over-long userhash overflowed a fixed-size decode buffer. + */ +static void +call_calc_helpers (void) +{ + enum MHD_DigestAuthAlgo3 a = algo3_tbl[cfg.hdr_seed + % (sizeof (algo3_tbl) + / sizeof (algo3_tbl[0]))]; + size_t hs; + size_t claim; + void *bin; + char *hex; + + hs = MHD_digest_get_hash_size (a); + if ( (0 == hs) || + (hs > MAX_DIGEST_BIN) ) + return; + claim = hs - (size_t) (cfg.crc_seed % (unsigned int) (hs + 1u)); + + bin = malloc (claim); + if (NULL != bin) + { + (void) MHD_digest_auth_calc_userdigest (a, + DIGEST_USER, + DIGEST_REALM, + DIGEST_PASS, + bin, + claim); + (void) MHD_digest_auth_calc_userhash (a, + DIGEST_USER, + DIGEST_REALM, + bin, + claim); + free (bin); + } + /* The hex form needs 2*hs+1 bytes; ask for less about half the time. */ + claim = (2u * hs + 1u) + - (size_t) (cfg.hdr_seed % (unsigned int) (2u * hs + 2u)); + hex = (char *) malloc (claim); + if (NULL != hex) + { + (void) MHD_digest_auth_calc_userhash_hex (a, + DIGEST_USER, + DIGEST_REALM, + hex, + claim); + free (hex); + } +} + + +/** + * Run the digest-authentication entry point selected by byte 5. + * + * Variant 0 is MHD_digest_auth_check3(), which is the plainest one and + * what the all-zero configuration selects. Variants 1-5 are the v1/v2 compatibility + * wrappers and the pre-computed-digest forms, which do their own buffer + * sizing and are therefore worth driving directly. Variants 6-8 are + * the query functions, which parse the client's Authorization header + * but return no verdict, so they run *in addition* to check3(). + * + * @return one of DAUTH_CHALLENGE, DAUTH_PASSED, DAUTH_STALE + */ +static int +run_digest_check (struct MHD_Connection *connection) +{ + unsigned int variant = cfg.dauth_variant; + enum MHD_DigestAuthResult dres; + uint8_t ud[MAX_DIGEST_BIN]; + size_t hs; + int r1; + volatile size_t sink = 0; + + switch (variant) + { + case 1: + r1 = MHD_digest_auth_check (connection, + DIGEST_REALM, + DIGEST_USER, + DIGEST_PASS, + 0); + if (MHD_YES == r1) + return DAUTH_PASSED; + return (MHD_INVALID_NONCE == r1) ? DAUTH_STALE : DAUTH_CHALLENGE; + case 2: + r1 = MHD_digest_auth_check2 (connection, + DIGEST_REALM, + DIGEST_USER, + DIGEST_PASS, + 0, + algo1_tbl[cfg.hdr_seed & 0x03]); + if (MHD_YES == r1) + return DAUTH_PASSED; + return (MHD_INVALID_NONCE == r1) ? DAUTH_STALE : DAUTH_CHALLENGE; + case 3: + /* MHD_digest_auth_check_digest() is MD5-only by definition. */ + if (MHD_YES != MHD_digest_auth_calc_userdigest ( + MHD_DIGEST_AUTH_ALGO3_MD5, + DIGEST_USER, DIGEST_REALM, DIGEST_PASS, + ud, MHD_MD5_DIGEST_SIZE)) + break; + r1 = MHD_digest_auth_check_digest (connection, + DIGEST_REALM, + DIGEST_USER, + ud, + 0); + if (MHD_YES == r1) + return DAUTH_PASSED; + return (MHD_INVALID_NONCE == r1) ? DAUTH_STALE : DAUTH_CHALLENGE; + case 4: + case 5: + { + /* The pre-computed-digest entry points are stricter than the + password-based ones: the digest and the algorithm have to + agree, and naming more than one base hashing algorithm is an + MHD_PANIC("API violation") rather than an error return. So the + algorithm is chosen first here and the digest, its length and + the algorithm argument are all derived from that one choice. + + MHD_DIGEST_ALG_AUTO is deliberately not used for variant 4: + MHD_digest_auth_check_digest2() maps it to + MULT_ALGO3_ANY_NON_SESSION, which names three base algorithms + and therefore trips that very panic. See README section 6. */ + static const enum MHD_DigestAuthAlgo3 digest_algos[] = { + MHD_DIGEST_AUTH_ALGO3_MD5, + MHD_DIGEST_AUTH_ALGO3_SHA256, + MHD_DIGEST_AUTH_ALGO3_SHA512_256, + MHD_DIGEST_AUTH_ALGO3_MD5_SESSION, + MHD_DIGEST_AUTH_ALGO3_SHA256_SESSION, + MHD_DIGEST_AUTH_ALGO3_SHA512_256_SESSION + }; + enum MHD_DigestAuthAlgo3 a3; + + if (4 == variant) + /* check_digest2() only knows MD5 and SHA-256. */ + a3 = (0 != (cfg.hdr_seed & 0x01)) + ? MHD_DIGEST_AUTH_ALGO3_SHA256 + : MHD_DIGEST_AUTH_ALGO3_MD5; + else + a3 = digest_algos[cfg.hdr_seed + % (sizeof (digest_algos) + / sizeof (digest_algos[0]))]; + hs = MHD_digest_get_hash_size (a3); + if ( (0 == hs) || + (hs > sizeof (ud)) || + (MHD_YES != MHD_digest_auth_calc_userdigest (a3, + DIGEST_USER, + DIGEST_REALM, + DIGEST_PASS, + ud, + hs)) ) + break; + if (4 == variant) + { + r1 = MHD_digest_auth_check_digest2 (connection, + DIGEST_REALM, + DIGEST_USER, + ud, + hs, + 0, + (MHD_DIGEST_AUTH_ALGO3_SHA256 == a3) + ? MHD_DIGEST_ALG_SHA256 + : MHD_DIGEST_ALG_MD5); + if (MHD_YES == r1) + return DAUTH_PASSED; + return (MHD_INVALID_NONCE == r1) ? DAUTH_STALE : DAUTH_CHALLENGE; + } + dres = MHD_digest_auth_check_digest3 (connection, + DIGEST_REALM, + DIGEST_USER, + ud, + hs, + 0, + 0, + cfg.qop, + /* Exactly one base algorithm. + The MULT_ALGO3_* constants are + numerically identical to their + ALGO3_* counterparts. */ + (enum MHD_DigestAuthMultiAlgo3) a3); + if (MHD_DAUTH_OK == dres) + return DAUTH_PASSED; + return (MHD_DAUTH_NONCE_STALE == dres) ? DAUTH_STALE : DAUTH_CHALLENGE; + } + case 6: + { + char *u = MHD_digest_auth_get_username (connection); + + if (NULL != u) + { + sink += strlen (u); + MHD_free (u); + } + } + break; + case 7: + { + struct MHD_DigestAuthUsernameInfo *ui; + + ui = MHD_digest_auth_get_username3 (connection); + if (NULL != ui) + { + if (NULL != ui->username) + sink += ui->username_len; + if (NULL != ui->userhash_hex) + sink += ui->userhash_hex_len; + MHD_free (ui); + } + } + break; + case 8: + { + struct MHD_DigestAuthInfo *ai; + + ai = MHD_digest_auth_get_request_info3 (connection); + if (NULL != ai) + { + if (NULL != ai->username) + sink += ai->username_len; + if (NULL != ai->realm) + sink += ai->realm_len; + if (NULL != ai->opaque) + sink += ai->opaque_len; + if (NULL != ai->userhash_hex) + sink += ai->userhash_hex_len; + sink += ai->cnonce_len; + sink += (size_t) ai->nc; + MHD_free (ai); + } + } + break; + default: + break; + } + (void) sink; + + /* Variant 0, the query variants and every early break end up here. */ + dres = MHD_digest_auth_check3 (connection, + DIGEST_REALM, + DIGEST_USER, + DIGEST_PASS, + 0 /* daemon default nonce timeout */, + 0 /* daemon default max_nc */, + cfg.qop, + MHD_DIGEST_AUTH_MULT_ALGO3_ANY_NON_SESSION); + if (MHD_DAUTH_OK == dres) + return DAUTH_PASSED; + return (MHD_DAUTH_NONCE_STALE == dres) ? DAUTH_STALE : DAUTH_CHALLENGE; +} + + +/** + * Send the "authentication required" reply through the variant of the + * API selected by byte 5. The v1/v2 forms take a plain response and an + * algorithm rather than the qop/algo pair of the v3 form. + */ +static enum MHD_Result +queue_auth_challenge (struct MHD_Connection *connection, + int stale) +{ + struct MHD_Response *resp; + enum MHD_Result ret; + + resp = MHD_create_response_from_buffer_static (0, ""); + if (NULL == resp) + return MHD_NO; + switch (cfg.fail_variant) + { + case 1: + ret = MHD_queue_auth_fail_response (connection, + DIGEST_REALM, + "0123456789abcdef", + resp, + stale ? MHD_YES : MHD_NO); + break; + case 2: + ret = MHD_queue_auth_fail_response2 (connection, + DIGEST_REALM, + "0123456789abcdef", + resp, + stale ? MHD_YES : MHD_NO, + algo1_tbl[cfg.hdr_seed & 0x03]); + break; + case 3: + if (0 != (cfg.hdr_seed & 0x01)) + ret = MHD_queue_basic_auth_required_response3 (connection, + DIGEST_REALM, + (0 != (cfg.hdr_seed + & 0x02)) + ? MHD_YES : MHD_NO, + resp); + else + ret = MHD_queue_basic_auth_fail_response (connection, + DIGEST_REALM, + resp); + break; + case 0: + default: + ret = MHD_queue_auth_required_response3 (connection, + DIGEST_REALM, + "0123456789abcdef" /* opaque */, + "/", + resp, + stale ? MHD_YES : MHD_NO, + cfg.qop, + cfg.algo, + MHD_YES /* userhash */, + MHD_NO); + break; + } + MHD_destroy_response (resp); + return ret; +} + + +/* ------------------------------------------------------------------ */ +/* Configuration byte 6: connection introspection */ +/* ------------------------------------------------------------------ */ + +/** + * Call the read-only connection accessors on attacker-supplied header + * data. MHD_lookup_connection_value*() run the case-insensitive header + * matcher over whatever the client sent, which is exactly the sort of + * input the fuzzer is good at producing. + */ +static void +exercise_connection_api (struct MHD_Connection *connection) +{ + volatile size_t sink = 0; + + if (cfg.conn_lookup) + { + static const char *const keys[] = { + "Host", "Content-Length", "Transfer-Encoding", "Authorization", + "Cookie", "", "X-Fuzz", "connection" + }; + const char *k = keys[cfg.hdr_seed % (sizeof (keys) / sizeof (keys[0]))]; + const char *v; + const char *vp = NULL; + size_t vlen = 0; + const char *uri = NULL; + size_t ulen = 0; + + v = MHD_lookup_connection_value (connection, MHD_HEADER_KIND, k); + if (NULL != v) + sink += strlen (v); + if (MHD_YES == MHD_lookup_connection_value_n (connection, + MHD_HEADER_KIND, + k, + strlen (k), + &vp, + &vlen)) + sink += vlen; + if (MHD_YES == MHD_get_connection_URI_path_n (connection, &uri, &ulen)) + sink += ulen; + } + if (cfg.conn_setvalue) + (void) MHD_set_connection_value (connection, + MHD_HEADER_KIND, + "X-Fuzz-Added", + "1"); + if (cfg.conn_info) + { + static const enum MHD_ConnectionInfoType cinfo[] = { + MHD_CONNECTION_INFO_CLIENT_ADDRESS, + MHD_CONNECTION_INFO_DAEMON, + MHD_CONNECTION_INFO_CONNECTION_FD, + MHD_CONNECTION_INFO_SOCKET_CONTEXT, + MHD_CONNECTION_INFO_CONNECTION_SUSPENDED, + MHD_CONNECTION_INFO_CONNECTION_TIMEOUT, + MHD_CONNECTION_INFO_REQUEST_HEADER_SIZE, + MHD_CONNECTION_INFO_HTTP_STATUS + }; + static const enum MHD_DaemonInfoType dinfo[] = { + MHD_DAEMON_INFO_CURRENT_CONNECTIONS, + MHD_DAEMON_INFO_FLAGS, + MHD_DAEMON_INFO_LISTEN_FD, + MHD_DAEMON_INFO_BIND_PORT + }; + unsigned int i; + + for (i = 0; i < sizeof (cinfo) / sizeof (cinfo[0]); i++) + if (NULL != MHD_get_connection_info (connection, cinfo[i])) + sink++; + for (i = 0; i < sizeof (dinfo) / sizeof (dinfo[0]); i++) + if ( (NULL != cur_daemon) && + (NULL != MHD_get_daemon_info (cur_daemon, dinfo[i])) ) + sink++; + } + if (cfg.conn_option) + (void) MHD_set_connection_option (connection, + MHD_CONNECTION_OPTION_TIMEOUT, + (unsigned int) (cfg.crc_seed & 0x0F)); + if (cfg.conn_info) + { + /* Constant-output accessors. They cannot fail on attacker input, + but they are cheap and leaving them uncovered makes the coverage + report harder to read. + MHD_fini() is deliberately NOT called anywhere: it tears down + process-global library state, so a single call would break every + later iteration in the same process. */ + const char *v = MHD_get_version (); + + if (NULL != v) + sink += strlen (v); + sink += (size_t) MHD_get_version_bin (); + } + (void) sink; +} + + +/** + * Suspend the connection if byte 7 asks for it. + * + * Mode 1 suspends and resumes straight away, which exercises both state + * transitions without leaving the connection parked. Mode 2 leaves it + * suspended and records it in @e pending_resume so that the pump loop + * resumes it on its next round; completed_cb() drops the record, so a + * connection MHD has finished with is never resumed after the fact, and + * the iteration flushes the whole set before stopping the daemon. A + * connection left suspended makes MHD_stop_daemon() MHD_PANIC(). + */ +static void +suspend_maybe (struct MHD_Connection *connection) +{ + if ( (0 == cfg.suspend_mode) || + tearing_down) + return; + MHD_suspend_connection (connection); + if (1 == cfg.suspend_mode) + { + MHD_resume_connection (connection); + return; + } + pending_resume_add (connection); +} + + static enum MHD_Result ahc (void *cls, struct MHD_Connection *connection, @@ -283,7 +1370,10 @@ ahc (void *cls, struct MHD_Response *resp; enum MHD_Result ret; volatile size_t sink = 0; + const char *conn_hdr = NULL; + char conn_lc[128]; + conn_lc[0] = '\0'; (void) cls; if (NULL == hs) { @@ -329,13 +1419,17 @@ ahc (void *cls, stat_final_calls++; if (cfg.do_iterate) { - (void) MHD_get_connection_values (connection, - MHD_HEADER_KIND - | MHD_GET_ARGUMENT_KIND - | MHD_COOKIE_KIND - | MHD_FOOTER_KIND, - &kv_iter, - NULL); + const enum MHD_ValueKind kinds = MHD_HEADER_KIND + | MHD_GET_ARGUMENT_KIND + | MHD_COOKIE_KIND + | MHD_FOOTER_KIND; + + /* Alternate between the NUL-terminated iterator and the + length-aware one, which are separate code paths in MHD. */ + if (0 != (cfg.hdr_seed & 0x10)) + (void) MHD_get_connection_values_n (connection, kinds, &kv_iter_n, NULL); + else + (void) MHD_get_connection_values (connection, kinds, &kv_iter, NULL); } if (oracle_on && @@ -344,61 +1438,129 @@ ahc (void *cls, "request body truncated: MHD completed the request but delivered " "fewer body bytes than the generated request contained"); - if (cfg.do_basic) + exercise_connection_api (connection); + + /* Lower-cased copy of the request's "Connection" header, used by the + upgrade check further down. MHD's own token matcher is internal, + so a plain case-folded substring test is used here; it only has to + be good enough to keep the harness from queueing an upgrade + response on a connection that cannot be upgraded. */ + if (cfg.allow_upgrade) { - struct MHD_BasicAuthInfo *bai; + conn_hdr = MHD_lookup_connection_value (connection, + MHD_HEADER_KIND, + MHD_HTTP_HEADER_CONNECTION); + if (NULL != conn_hdr) + { + size_t i; + + for (i = 0; (i + 1 < sizeof (conn_lc)) && ('\0' != conn_hdr[i]); i++) + conn_lc[i] = (char) (( ('A' <= conn_hdr[i]) && ('Z' >= conn_hdr[i])) + ? (conn_hdr[i] - 'A' + 'a') : conn_hdr[i]); + conn_lc[i] = '\0'; + } + } - bai = MHD_basic_auth_get_username_password3 (connection); - if (NULL != bai) + if (cfg.do_basic) + { + if (cfg.basic_v1) { - if (NULL != bai->username) - sink += bai->username_len; - if (NULL != bai->password) - sink += bai->password_len; - MHD_free (bai); + /* The v1 getter returns two separate malloc()ed strings and is + the one that historically had to be freed by hand. */ + char *pass = NULL; + char *user; + + user = MHD_basic_auth_get_username_password (connection, &pass); + if (NULL != user) + sink += strlen (user); + if (NULL != pass) + sink += strlen (pass); + if (NULL != user) + MHD_free (user); + if (NULL != pass) + MHD_free (pass); + } + else + { + struct MHD_BasicAuthInfo *bai; + + bai = MHD_basic_auth_get_username_password3 (connection); + if (NULL != bai) + { + if (NULL != bai->username) + sink += bai->username_len; + if (NULL != bai->password) + sink += bai->password_len; + MHD_free (bai); + } } } + if (cfg.calc_helpers) + call_calc_helpers (); + if (cfg.do_digest) { - enum MHD_DigestAuthResult dres; - - dres = MHD_digest_auth_check3 (connection, - DIGEST_REALM, - DIGEST_USER, - DIGEST_PASS, - 0 /* daemon default nonce timeout */, - 0 /* daemon default max_nc */, - cfg.qop, - MHD_DIGEST_AUTH_MULT_ALGO3_ANY_NON_SESSION); - if (MHD_DAUTH_OK == dres) + int dres = run_digest_check (connection); + + if (DAUTH_PASSED == dres) stat_auth_ok++; else { stat_challenges++; - resp = MHD_create_response_from_buffer_static (0, ""); - if (NULL == resp) - return MHD_NO; - ret = MHD_queue_auth_required_response3 (connection, - DIGEST_REALM, - "0123456789abcdef" /* opaque */, - "/", - resp, - (MHD_DAUTH_NONCE_STALE == dres) - ? MHD_YES : MHD_NO, - cfg.qop, - cfg.algo, - MHD_YES /* userhash */, - MHD_NO); + return queue_auth_challenge (connection, DAUTH_STALE == dres); + } + } + + /* Only answer 101 for a request that really is an upgrade request: + HTTP/1.1, an "Upgrade" header, and a "Connection" header naming the + upgrade token (RFC 9110 section 7.8). This is the check a real + application performs, and all three inputs come off the wire, so + the path stays fully attacker-driven. + + It is deliberately NOT enough to keep MHD's + mhd_assert (NULL == r->upgrade_handler || + MHD_CONN_MUST_UPGRADE == c->keepalive) + in build_header_response() satisfied, and it is not meant to be: + MHD may have decided during header parsing that the connection must + close, and does not expose that decision. That is finding K7 (see + README section 6); corpus/known-findings/K7-upgrade-after-must-close.bin + is the reproducer, and patches/K7.diff turns the abort into the + MHD_NO that the code below already handles. Do not "fix" this by + weakening the condition here -- the check above is what an + application can actually do, and the harness has to behave like + one. */ + if (cfg.allow_upgrade && + (NULL != version) && + (0 == strcmp (version, MHD_HTTP_VERSION_1_1)) && + (NULL != MHD_lookup_connection_value (connection, + MHD_HEADER_KIND, + MHD_HTTP_HEADER_UPGRADE)) && + (NULL != conn_hdr) && + (NULL != strstr (conn_lc, "upgrade")) && + (NULL == strstr (conn_lc, "close"))) + { + resp = MHD_create_response_for_upgrade (&upgrade_cb, NULL); + if (NULL != resp) + { + /* Mandatory: MHD_create_response_for_upgrade() documents that the + upgrade headers are the application's job, and + MHD_response_execute_upgrade_() asserts that "Upgrade" is + present before it hands the socket over. Omitting it is an + application-contract violation, not something worth fuzzing. */ + (void) MHD_add_response_header (resp, + MHD_HTTP_HEADER_UPGRADE, + "fuzz-protocol"); + decorate_response (resp); + ret = MHD_queue_response (connection, + MHD_HTTP_SWITCHING_PROTOCOLS, + resp); MHD_destroy_response (resp); return ret; } } - if (cfg.chunked_reply) - resp = MHD_create_response_from_buffer_copy (11, "hello world"); - else - resp = MHD_create_response_from_buffer_static (2, "ok"); + resp = make_response (); if (NULL == resp) return MHD_NO; ret = MHD_queue_response (connection, @@ -406,6 +1568,17 @@ ahc (void *cls, ? MHD_HTTP_FORBIDDEN : MHD_HTTP_OK, resp); MHD_destroy_response (resp); + /* Only after a response was actually accepted. Returning MHD_NO + tells MHD to terminate the connection, and terminating a connection + that this callback has just suspended trips + mhd_assert (! connection->suspended) in MHD_connection_close_(). + That is the harness contradicting itself, not an MHD defect: the + two requests are mutually exclusive by construction. (MHD_NO is + reachable here for real -- MHD_queue_response() rejects, for + instance, a response carrying MHD_RF_HEAD_ONLY_RESPONSE on a GET, + which decorate_response() can set.) */ + if (MHD_YES == ret) + suspend_maybe (connection); return ret; } @@ -419,8 +1592,10 @@ completed_cb (void *cls, struct hstate *hs = (struct hstate *) *req_cls; (void) cls; - (void) connection; (void) toe; + /* MHD is done with this connection, so the deferred resume of + suspend mode 2 must not fire for it any more. */ + pending_resume_drop (connection); if (NULL == hs) return; if (NULL != hs->pp) @@ -497,6 +1672,84 @@ drain_and_harvest (int sock) } +/** + * Advance the daemon by one cycle, through the event-loop API selected + * by byte 6 of the input. + * + * Mode 0 is MHD_run(), which is what the harness used before the + * extension and which does the descriptor bookkeeping internally. + * Modes 1-3 drive the daemon the way an application with its own event + * loop does: MHD_get_fdset*() to collect the descriptors, select(), then + * MHD_run_from_select*(). That is the most common production + * integration and none of it was reachable before. + * + * The select() timeout is always zero. The harness is single threaded + * and whatever the daemon is waiting for has already been written into + * the socketpair, so blocking would only burn wall clock; select() is + * called purely to populate the ready sets. + */ +static void +run_once (struct MHD_Daemon *d) +{ + fd_set rs; + fd_set ws; + fd_set es; + MHD_socket max_fd = MHD_INVALID_SOCKET; + struct timeval tv; + + if (0 == cfg.loop_mode) + { + (void) MHD_run (d); + return; + } + FD_ZERO (&rs); + FD_ZERO (&ws); + FD_ZERO (&es); + if (1 == cfg.loop_mode) + { + /* Parenthesised so that the real v1 function is called: + microhttpd.h also defines MHD_get_fdset as a macro that forwards + to MHD_get_fdset2 with FD_SETSIZE, so the unparenthesised name + would never reach the v1 entry point at all. Same trick for + MHD_run_from_select below. */ + if (MHD_YES != (MHD_get_fdset) (d, &rs, &ws, &es, &max_fd)) + { + (void) MHD_run (d); + return; + } + } + else + { + if (MHD_YES != MHD_get_fdset2 (d, &rs, &ws, &es, &max_fd, + (unsigned int) FD_SETSIZE)) + { + (void) MHD_run (d); + return; + } + } + if (cfg.use_timeouts) + { + MHD_UNSIGNED_LONG_LONG tl = 0; + uint64_t t64 = 0; + volatile int64_t sink; + + (void) MHD_get_timeout (d, &tl); + (void) MHD_get_timeout64 (d, &t64); + sink = MHD_get_timeout64s (d); + sink += (int64_t) MHD_get_timeout_i (d); + (void) sink; + } + tv.tv_sec = 0; + tv.tv_usec = 0; + if (MHD_INVALID_SOCKET != max_fd) + (void) select ((int) max_fd + 1, &rs, &ws, &es, &tv); + if (1 == cfg.loop_mode) + (void) (MHD_run_from_select) (d, &rs, &ws, &es); + else + (void) MHD_run_from_select2 (d, &rs, &ws, &es, (unsigned int) FD_SETSIZE); +} + + static void pump (struct MHD_Daemon *d, int sock, @@ -506,7 +1759,12 @@ pump (struct MHD_Daemon *d, for (i = 0; i < rounds; i++) { - (void) MHD_run (d); + /* Deferred resume of suspend mode 2. Each slot is cleared before + its connection is resumed, which keeps this correct even if the + resume makes MHD complete (and forget) the connection, or run the + handler again so that it suspends the same one anew. */ + (void) pending_resume_flush (); + run_once (d); drain_and_harvest (sock); } } @@ -646,7 +1904,9 @@ LLVMFuzzerTestOneInput (const uint8_t *data, in fuzz_common.h for why the process dies without it. */ fuzz_ignore_sigpipe (); - if (size < 6) + /* The ten configuration bytes are mandatory. Anything shorter has + no segment stream either, so there is nothing to fuzz. */ + if (size < 10) return 0; nonce_len = 0; @@ -662,6 +1922,10 @@ LLVMFuzzerTestOneInput (const uint8_t *data, cfg.do_basic = (0 != (data[1] & 0x02)); cfg.do_postproc = (0 != (data[1] & 0x04)); cfg.do_iterate = (0 != (data[1] & 0x08)); + /* Bits 0x10 and 0x80 of byte 1 are unused. 0x10 used to gate the + configuration bytes 4-9 back when they were optional; it is left + free rather than reassigned so that the meaning of the corpus + files written while it was a gate does not silently change. */ cfg.chunked_reply = (0 != (data[1] & 0x20)); cfg.error_reply = (0 != (data[1] & 0x40)); if (! min_mem_limit_read) @@ -695,8 +1959,43 @@ LLVMFuzzerTestOneInput (const uint8_t *data, : MHD_DIGEST_AUTH_MULT_QOP_ANY_NON_INT; cfg.nonce_nc_size = nnc_tbl[(data[3] >> 4) & 0x03]; + cfg.resp_kind = (unsigned int) (data[4] & 0x0F); + cfg.resp_nhdr = (unsigned int) ((data[4] >> 4) & 0x03); + cfg.resp_footer = (0 != (data[4] & 0x40)); + cfg.resp_hdr_api = (0 != (data[4] & 0x80)); + + cfg.dauth_variant = (unsigned int) (data[5] & 0x0F) % DAUTH_VARIANT_COUNT; + cfg.fail_variant = (unsigned int) ((data[5] >> 4) & 0x03); + cfg.basic_v1 = (0 != (data[5] & 0x40)); + cfg.calc_helpers = (0 != (data[5] & 0x80)); + + cfg.loop_mode = (unsigned int) (data[6] & 0x03); + cfg.use_timeouts = (0 != (data[6] & 0x04)); + cfg.conn_lookup = (0 != (data[6] & 0x08)); + cfg.conn_setvalue = (0 != (data[6] & 0x10)); + cfg.conn_info = (0 != (data[6] & 0x20)); + cfg.conn_option = (0 != (data[6] & 0x40)); + cfg.quiesce = (0 != (data[6] & 0x80)); + + /* Only modes 0-2 exist; 3 folds onto the immediate-resume mode + rather than being a fourth, unimplemented behaviour. */ + cfg.suspend_mode = (unsigned int) (data[7] & 0x03); + if (3 == cfg.suspend_mode) + cfg.suspend_mode = 1; + cfg.allow_upgrade = + (0 != (data[7] & 0x04)) && + (MHD_YES == MHD_is_feature_supported (MHD_FEATURE_UPGRADE)); + cfg.upgrade_action = (unsigned int) ((data[7] >> 3) & 0x03); + + cfg.hdr_seed = data[8]; + cfg.crc_seed = data[9]; + oracle_on = 0; expect_body_len = 0; + /* Must never carry over: the connections these pointed at belonged to + the previous iteration's daemon and are long gone. */ + memset (pending_resume, 0, sizeof (pending_resume)); + tearing_down = 0; if (0 != cfg.mem_limit) { @@ -728,6 +2027,10 @@ LLVMFuzzerTestOneInput (const uint8_t *data, flags = MHD_USE_NO_LISTEN_SOCKET; if (fuzz_verbose) flags |= MHD_USE_ERROR_LOG; + if (0 != cfg.suspend_mode) + flags |= MHD_ALLOW_SUSPEND_RESUME; + if (cfg.allow_upgrade) + flags |= MHD_ALLOW_UPGRADE; MHD_set_panic_func (&panic_cb, NULL); d = MHD_start_daemon (flags, @@ -743,6 +2046,7 @@ LLVMFuzzerTestOneInput (const uint8_t *data, MHD_OPTION_END); if (NULL == d) return 0; + cur_daemon = d; stat_daemons++; if (! stats_registered) { @@ -753,10 +2057,11 @@ LLVMFuzzerTestOneInput (const uint8_t *data, if (0 != new_connection (d, &sock)) { MHD_stop_daemon (d); + cur_daemon = NULL; return 0; } - pos = 4; + pos = 10u; while ( (pos + 2 <= size) && (nseg < MAX_SEGMENTS) ) { @@ -803,7 +2108,28 @@ LLVMFuzzerTestOneInput (const uint8_t *data, close_connection (d, &sock); pump (d, sock, 4); + /* A connection left suspended makes MHD_stop_daemon() MHD_PANIC() + ("called while we have suspended connections"), and + MHD_resume_connection() alone is not enough: it only raises a flag, + and the connection is taken off the daemon's suspended list by + MHD_run(). So flush and run until nothing is parked any more. + tearing_down stops the handler from parking anything new, which is + what bounds this loop; the cap is only a backstop. */ + tearing_down = 1; + { + unsigned int i; + + for (i = 0; i < MAX_CONNECTIONS + 2u; i++) + { + if (! pending_resume_flush ()) + break; + (void) MHD_run (d); + } + } + if (cfg.quiesce) + (void) MHD_quiesce_daemon (d); MHD_stop_daemon (d); + cur_daemon = NULL; return 0; } @@ -1421,14 +2747,18 @@ struct seed_def const char *name; unsigned char cfg[4]; struct seed_part parts[4]; + /* Configuration bytes 4-9. Declared after @e parts so that a seed + which only cares about the wire data can leave the member out of + its brace initialiser and get the all-zero (plainest) setting. */ + unsigned char ext[6]; }; #define P_END { 0, NULL } static const struct seed_def seeds[] = { - { "plain", { 0x00, 0x08, 0x03, 0x00 }, - { { 0, "GET / HTTP/1.1\r\nHost: x\r\n\r\n" }, P_END, P_END, P_END } }, - + /* There is deliberately no bare "GET / HTTP/1.1" seed: every seed + below opens with a request at least as simple, so a plain one adds + no coverage of its own (libFuzzer -merge=1 drops it). */ { "content-length-body", { 0x00, 0x08, 0x03, 0x00 }, { { 1, "hello" }, { 0, "POST /a HTTP/1.1\r\nHost: x\r\nContent-Length: 5\r\n\r\nhello" }, @@ -1516,7 +2846,174 @@ static const struct seed_def seeds[] = { { "pipelined", { 0x00, 0x08, 0x03, 0x00 }, { { 0, "GET /a HTTP/1.1\r\nHost: x\r\n\r\n" "GET /b?q HTTP/1.1\r\nHost: x\r\n\r\n" }, - P_END, P_END, P_END } } + P_END, P_END, P_END } }, + + /* ------------- seeds driving configuration bytes 4-9 ------------- + The seeds above leave those bytes zero and only exercise the + parser. Without at least one seed per area below, the rest of the + API stays practically unreachable: libFuzzer would have to guess + six configuration bytes before anything new runs. Each seed turns + on exactly one area so that a minimiser keeps them distinguishable. + ext[] is { response, auth, event-loop/introspection, suspend and + upgrade, header seed, content-reader seed }. */ + + /* Chunked reply produced by a content-reader callback, with response + headers and a trailer -- the reply-side counterpart of the chunk + parsing that commit c13f4c64 fixed on the request side. */ + { "ext-callback-chunked", { 0x00, 0x08, 0x03, 0x00 }, + { { 0, "GET / HTTP/1.1\r\nHost: x\r\n\r\n" }, P_END, P_END, P_END }, + { 0x0A | 0x20 | 0x40, 0x00, 0x00, 0x00, 0x01, 0x20 } }, + + /* Known-length callback body, with a reader error injected part way + through (crc_seed bit 0x80). */ + { "ext-callback-error", { 0x00, 0x08, 0x03, 0x00 }, + { { 0, "GET / HTTP/1.1\r\nHost: x\r\n\r\n" }, P_END, P_END, P_END }, + { 0x09, 0x00, 0x00, 0x00, 0x02, 0x82 } }, + + { "ext-fd-response", { 0x00, 0x08, 0x03, 0x00 }, + { { 0, "GET / HTTP/1.1\r\nHost: x\r\n\r\n" }, P_END, P_END, P_END }, + { 0x0B, 0x00, 0x00, 0x00, 0x00, 0x10 } }, + + { "ext-fd-at-offset-response", { 0x00, 0x08, 0x03, 0x00 }, + { { 0, "GET / HTTP/1.1\r\nHost: x\r\n\r\n" }, P_END, P_END, P_END }, + { 0x0C, 0x00, 0x00, 0x00, 0x00, 0x10 } }, + + { "ext-pipe-response", { 0x00, 0x08, 0x03, 0x00 }, + { { 0, "GET / HTTP/1.1\r\nHost: x\r\n\r\n" }, P_END, P_END, P_END }, + { 0x0E, 0x00, 0x00, 0x00, 0x00, 0x10 } }, + + { "ext-iovec-response", { 0x00, 0x08, 0x03, 0x00 }, + { { 0, "GET / HTTP/1.1\r\nHost: x\r\n\r\n" }, P_END, P_END, P_END }, + { 0x0F, 0x00, 0x00, 0x00, 0x00, 0x07 } }, + + /* Empty response plus the header manipulation API (get/del/options). */ + { "ext-empty-response-hdrapi", { 0x00, 0x08, 0x03, 0x00 }, + { { 0, "GET / HTTP/1.1\r\nHost: x\r\n\r\n" }, P_END, P_END, P_END }, + { 0x02 | 0x30 | 0x40 | 0x80, 0x00, 0x00, 0x00, 0x05, 0x00 } }, + + /* Digest through the pre-computed-userdigest entry point. */ + { "ext-digest-check-digest3", { 0x00, 0x09, 0x03, 0x00 }, + { { 0, "GET / HTTP/1.1\r\nHost: x\r\n\r\n" }, + { 0, "GET / HTTP/1.1\r\nHost: x\r\nAuthorization: Digest " + "username=\"user\", realm=\"TestRealm\", nonce=\"%%NONCE%%\", " + "uri=\"/\", response=\"00000000000000000000000000000000\"\r\n\r\n" }, + P_END, P_END }, + { 0x00, 0x05 | 0x80, 0x00, 0x00, 0x01, 0x00 } }, + + /* The v1 wrappers and the request-info query. */ + { "ext-digest-v1-wrappers", { 0x00, 0x09, 0x03, 0x00 }, + { { 0, "GET / HTTP/1.1\r\nHost: x\r\n\r\n" }, P_END, P_END, P_END }, + { 0x00, 0x01 | 0x10, 0x00, 0x00, 0x00, 0x00 } }, + + { "ext-digest-request-info", { 0x00, 0x09, 0x03, 0x00 }, + { { 0, "GET / HTTP/1.1\r\nHost: x\r\n\r\n" }, P_END, P_END, P_END }, + { 0x00, 0x08 | 0x20, 0x00, 0x00, 0x03, 0x00 } }, + + /* Application-driven event loop: MHD_get_fdset2() + select() + + MHD_run_from_select2(), with the timeout accessors. */ + { "ext-external-event-loop", { 0x00, 0x08, 0x03, 0x00 }, + { { 0, "GET / HTTP/1.1\r\nHost: x\r\n\r\n" }, P_END, P_END, P_END }, + { 0x01, 0x00, 0x02 | 0x04, 0x00, 0x00, 0x00 } }, + + /* The v1 fdset/run_from_select pair. */ + { "ext-external-event-loop-v1", { 0x00, 0x08, 0x03, 0x00 }, + { { 0, "GET / HTTP/1.1\r\nHost: x\r\n\r\n" }, P_END, P_END, P_END }, + { 0x01, 0x00, 0x01 | 0x04, 0x00, 0x00, 0x00 } }, + + /* Every connection-introspection accessor at once. */ + { "ext-connection-api", { 0x00, 0x08, 0x03, 0x00 }, + { { 0, "GET /path?a=1 HTTP/1.1\r\nHost: x\r\nCookie: a=b\r\n\r\n" }, + P_END, P_END, P_END }, + { 0x01, 0x00, 0x08 | 0x10 | 0x20 | 0x40 | 0x80, 0x00, 0x10, 0x00 } }, + + /* Suspend the connection and resume it from the pump loop. */ + { "ext-suspend-resume", { 0x00, 0x08, 0x03, 0x00 }, + { { 0, "GET / HTTP/1.1\r\nHost: x\r\n\r\n" }, + { 2, "" }, P_END, P_END }, + { 0x01, 0x00, 0x00, 0x02, 0x00, 0x00 } }, + + /* Two connections parked at the same time (op 3 opens the second + without waiting for the first). The bookkeeping behind suspend + mode 2 has to be a set for this: while it was a single slot the + first connection was forgotten and stayed suspended, and + MHD_stop_daemon() answered with + MHD_PANIC ("called while we have suspended connections"). */ + { "ext-suspend-two-connections", { 0x00, 0x08, 0x03, 0x00 }, + { { 0, "GET /a HTTP/1.1\r\nHost: x\r\n\r\n" }, + { 3, "GET /b HTTP/1.1\r\nHost: x\r\n\r\n" }, + { 2, "" }, P_END }, + { 0x01, 0x00, 0x00, 0x02, 0x00, 0x00 } }, + + /* HTTP "Upgrade": the client asks for it and the handler answers 101 + with an upgrade response, after which the socket is handed over. */ + { "ext-upgrade", { 0x00, 0x08, 0x03, 0x00 }, + { { 0, "GET / HTTP/1.1\r\nHost: x\r\nConnection: Upgrade\r\n" + "Upgrade: fuzz-protocol\r\n\r\n" }, + { 2, "" }, P_END, P_END }, + { 0x01, 0x00, 0x00, 0x04 | 0x08, 0x00, 0x00 } }, + + /* The remaining response constructors. One seed each, because the + constructor is picked by the low nibble of ext[0] and a seed that + does not name it leaves that entry point unreachable until the + fuzzer happens to mutate the nibble. */ + { "ext-buffer-persistent", { 0x00, 0x08, 0x03, 0x00 }, + { { 0, "GET / HTTP/1.1\r\nHost: x\r\n\r\n" }, P_END, P_END, P_END }, + { 0x03, 0x00, 0x00, 0x00, 0x00, 0x00 } }, + + { "ext-buffer-free-callback", { 0x00, 0x08, 0x03, 0x00 }, + { { 0, "GET / HTTP/1.1\r\nHost: x\r\n\r\n" }, P_END, P_END, P_END }, + { 0x06, 0x00, 0x00, 0x00, 0x00, 0x00 } }, + + { "ext-from-data", { 0x00, 0x08, 0x03, 0x00 }, + { { 0, "GET / HTTP/1.1\r\nHost: x\r\n\r\n" }, P_END, P_END, P_END }, + { 0x07, 0x00, 0x00, 0x00, 0x00, 0x00 } }, + + { "ext-fd64-response", { 0x00, 0x08, 0x03, 0x00 }, + { { 0, "GET / HTTP/1.1\r\nHost: x\r\n\r\n" }, P_END, P_END, P_END }, + { 0x0D, 0x00, 0x00, 0x00, 0x00, 0x00 } }, + + /* The v1 basic-auth getter, which returns two separate malloc()ed + strings rather than one struct. */ + { "ext-basic-auth-v1", { 0x00, 0x0a, 0x03, 0x00 }, + { { 0, "GET / HTTP/1.1\r\nHost: x\r\n" + "Authorization: Basic dXNlcjpwYXNz\r\n\r\n" }, + P_END, P_END, P_END }, + { 0x01, 0x40, 0x00, 0x00, 0x00, 0x00 } }, + + /* Answer the challenge with the basic-auth responses instead of the + digest ones (fail_variant 3). */ + { "ext-basic-auth-challenge", { 0x00, 0x09, 0x03, 0x00 }, + { { 0, "GET / HTTP/1.1\r\nHost: x\r\n\r\n" }, P_END, P_END, P_END }, + { 0x01, 0x30, 0x00, 0x00, 0x00, 0x00 } }, + + { "ext-basic-auth-challenge-utf8", { 0x00, 0x09, 0x03, 0x00 }, + { { 0, "GET / HTTP/1.1\r\nHost: x\r\n\r\n" }, P_END, P_END, P_END }, + { 0x01, 0x30, 0x00, 0x00, 0x03, 0x00 } }, + + /* The MD5-only and algorithm-parametrised pre-computed digest + wrappers. */ + { "ext-digest-check-digest-v1", { 0x00, 0x09, 0x03, 0x00 }, + { { 0, "GET / HTTP/1.1\r\nHost: x\r\n\r\n" }, P_END, P_END, P_END }, + { 0x01, 0x03, 0x00, 0x00, 0x00, 0x00 } }, + + { "ext-digest-check-digest2", { 0x00, 0x09, 0x03, 0x00 }, + { { 0, "GET / HTTP/1.1\r\nHost: x\r\n\r\n" }, P_END, P_END, P_END }, + { 0x01, 0x04, 0x00, 0x00, 0x01, 0x00 } }, + + /* The username queries. */ + { "ext-digest-get-username-v1", { 0x00, 0x09, 0x03, 0x00 }, + { { 0, "GET / HTTP/1.1\r\nHost: x\r\n" + "Authorization: Digest username=\"user\", realm=\"TestRealm\"\r\n\r\n" } + , + P_END, P_END, P_END }, + { 0x01, 0x06, 0x00, 0x00, 0x00, 0x00 } }, + + { "ext-digest-get-username3", { 0x00, 0x09, 0x03, 0x00 }, + { { 0, "GET / HTTP/1.1\r\nHost: x\r\n" + "Authorization: Digest userhash=true, username=\"aabb\", " + "realm=\"TestRealm\"\r\n\r\n" }, + P_END, P_END, P_END }, + { 0x01, 0x07, 0x00, 0x00, 0x00, 0x00 } } }; static uint8_t seed_render_buf[4096]; @@ -1541,6 +3038,7 @@ fuzz_seed_get (size_t idx, b.len = 0; b.cap = sizeof (seed_render_buf); sb_raw (&b, sd->cfg, 4); + sb_raw (&b, sd->ext, sizeof (sd->ext)); for (i = 0; i < sizeof (sd->parts) / sizeof (sd->parts[0]); i++) { size_t n;